imdone-core
Version:
353 lines (299 loc) • 10.3 kB
JavaScript
import _path from 'path'
import _isFunction from 'lodash.isfunction'
import _isUndefined from 'lodash.isundefined'
import _assign from 'lodash.assign'
import _remove from 'lodash.remove'
import _union from 'lodash.union'
import { eachLimit } from 'async'
import { computeChecksum } from '../checksum.js'
import { constants } from '../constants.js'
import debug from 'debug'
const log = debug('imdone-mixins:repo-fs-store.js')
import { File } from '../file.js'
import { Config } from '../config.js'
import { languages } from '../languages.js'
import { isBinaryFile } from 'isbinaryfile'
import eol from 'eol'
const _eol = String(eol.auto)
import readdirp from 'readdirp'
import _ignore from 'ignore'
import { sort } from 'fast-sort'
import { buildMigrateConfig } from '../migrate-config.js'
import { loadYAML, dumpYAML } from '../adapters/yaml.js';
import realFs from 'fs'
import { preparePathForWriting, readFile, writeFile, stat, lstat, unlink } from '../adapters/file-gateway.js'
const {
CONFIG_FILE_YML,
ASYNC_LIMIT,
DEFAULT_IGNORE_PATHS,
DEFAULT_IGNORE_EXTS,
IGNORE_FILE,
SORT_FILE,
ERRORS,
} = constants
const { DEFAULT_CONFIG } = Config
export function fileSystemStoreMixin(repo, fs = realFs) {
repo.allFiles = []
function getIgnorePatterns() {
var ignoreFile = _path.join(repo.path, IGNORE_FILE)
let patterns = DEFAULT_IGNORE_PATHS
if (fs.existsSync(ignoreFile))
patterns = patterns + _eol + fs.readFileSync(ignoreFile).toString()
return patterns
}
const IG = _ignore().add(getIgnorePatterns())
function shouldIncludeFile(entry) {
const { path, dirent } = entry
return !dirent.isSymbolicLink() && !IG.ignores(path)
}
repo.getAllFilePaths = async function (root) {
const entries = []
for await (const entry of readdirp(root, { lstat: true, fileFilter: shouldIncludeFile })) {
entries.push(entry)
}
return entries.filter((res) => repo.shouldInclude(res.path))
.map((res) => res.fullPath)
}
repo.addFilePath = async function (path) {
var relPath = repo.getRelativePath(path)
var fullPath = repo.getFullPath(path)
const stat = await fs.promises.lstat(fullPath)
if (stat.isDirectory()) relPath += _path.sep
if (!repo.allFiles.includes(relPath)) {
repo.allFiles.push(relPath)
}
}
repo.removeFilePath = function (path) {
_remove(repo.allFiles, (item) => item === path)
}
repo.getFilePaths = function () {
return repo.allFiles
}
repo.migrateConfig = buildMigrateConfig(repo)
// READY Refactor init to use async/await
// #esm-migration #important #urgent
// <!--
// order:-270
// -->
repo.init = async function () {
const stats = await stat(repo.path)
if (!stats.isDirectory()) {
var error = new Error(
`Path must be an existing directory on the file system: ${repo.path}`
)
repo.emit('error', error)
throw error
}
await repo.loadConfig()
repo.migrateConfig()
await repo.saveConfig()
repo.emit('config.loaded')
repo.createListeners()
const files = await repo.readFiles()
const lists = repo.getTasksByList()
repo.emit('fs-store-initialized', { ok: true, lists })
return files
}
repo.loadIgnore = function () {
const patterns = getIgnorePatterns() + _eol + DEFAULT_IGNORE_EXTS
repo.setIgnores(patterns)
}
repo.fileOK = async function (file, includeDirs = false) {
if (File.isFile(file)) file = file.path
if (!file || !this.shouldInclude(file)) return false
var fullPath = repo.getFullPath(file)
try {
const stats = await lstat(fullPath)
if (!stats) return false
if (/\.\.(\/|\\)/.test(file) || (!includeDirs && stats.isDirectory()))
return false
const content = await readFile(fullPath)
if (await isBinaryFile(content, stats.size) ) {
return false
}
return stats
} catch (err) {
return false
}
}
// READY Refactor saveSort to use async/await
// #esm-migration #important
// TODO readSort and saveSort are no longer used
repo.saveSort = async function (sort) {
writeFile(repo.getFullPath(SORT_FILE), JSON.stringify(sort), 'utf8')
}
repo.readSort = async function () {
const data = readFile(repo.getFullPath(SORT_FILE))
repo.sort = JSON.parse(data)
}
repo.saveConfig = async function () {
try {
repo.savingConfig = true
const file = repo.getFullPath(CONFIG_FILE_YML)
const data = { ...repo.getConfig() }
delete data.cardActionsFunction
delete data.boardActionsFunction
delete data.cardPropertiesFunction
delete data.path
if (data.lists) data.lists.forEach((list) => delete list.tasks)
const yaml = dumpYAML(data)
await preparePathForWriting(file)
await writeFile(file, yaml)
} finally {
repo.savingConfig = false
}
}
repo.updateConfig = async function (loadedConfig) {
var baseConfig = structuredClone(DEFAULT_CONFIG)
var include_lists = baseConfig.code.include_lists
const currentConfig = repo.config
if (currentConfig && currentConfig.code)
include_lists = _union(include_lists, currentConfig.code.include_lists)
if (currentConfig && currentConfig.settings)
loadedConfig.settings = {...currentConfig.settings, ...loadedConfig.settings}
if (loadedConfig && loadedConfig.code)
include_lists = _union(include_lists, loadedConfig.code.include_lists)
const newConfig = new Config(
_assign({}, baseConfig, repo.config, loadedConfig, { path: repo.path })
)
await repo.migrateTasksByConfig(currentConfig, newConfig)
repo.config = newConfig
repo.config.code.include_lists = include_lists
var _languages = structuredClone(languages)
repo.languages = repo.config.languages
? _assign(_languages, repo.config.languages)
: _languages
return repo.config
}
repo.loadConfig = async function (cb) {
// BACKLOG If a config is bad move it to config.json.bak and save a new one with defaults gh:2 id:9 +enhancement +standup +chore
// <!--
// order:-670
// -->
repo.loadIgnore()
const configData = (!repo.config || repo.config.dirty)
? await this.getYamlConfig()
: repo.config || {}
return await repo.updateConfig(configData)
}
repo.getConfigFile = function () {
return repo.getFullPath(CONFIG_FILE_YML)
}
repo.getYamlConfig = async function () {
const filePath = repo.getFullPath(CONFIG_FILE_YML)
if (!fs.existsSync(filePath)) return
const configData = await readFile(filePath, 'utf-8')
let configLike = {}
try {
configLike = loadYAML(configData.toString())
} catch (e) {
/* noop */
}
return configLike
}
// READY Refactor writeFile to use async/await
// #esm-migration #important
// <!--
// order:-280
// -->
repo.writeFile = async (file, emitFileUpdate) => {
if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
if (file.deleted) return file
if (/\.\.(\/|\\)/.test(file.path)) throw new Error('Unable to write file:' + file.path)
const filePath = repo.getFullPath(file)
await preparePathForWriting(filePath)
const oldChecksum = file.checksum
file.checksum = computeChecksum(file.getContentForFile())
try {
await writeFile(filePath, file.getContentForFile(), 'utf8')
} catch (err) {
file.checksum = oldChecksum
throw new Error('Unable to write file:' + filePath, {cause: err})
}
try {
const stats = await stat(filePath)
file.setModifiedTime(stats.mtime)
file.modified = false
repo.addFilePath(filePath)
if (emitFileUpdate) repo.emit('file.saved', file)
} catch (err) {
throw new Error('Unable to stat file:' + filePath, {cause: err})
}
return file
}
// READY Refactor getFilesInPath to use async/await
// #esm-migration #important #urgent
// <!--
// order:-20
// -->
repo.getFilesInPath = async function (includeDirs) {
const allPaths = await repo.getAllFilePaths(repo.path)
const files = []
let processed = 0
log('allPaths=', allPaths)
if (allPaths.length === 0) return files
await eachLimit(
allPaths,
ASYNC_LIMIT,
async (path) => {
if (!path) return
path = repo.getRelativePath(path)
const stat = await repo.fileOK(path, includeDirs)
processed++
repo.emit('file.processed', {
file: path,
ok: stat !== false,
total: allPaths.length,
processed: processed + 1,
repoId: repo.getId(),
})
if (stat) {
log('%s is ok %j', path, stat)
var file = new File({
repoId: repo.getId(),
filePath: path,
modifiedTime: stat.mtime,
createdTime: stat.birthtime,
languages: repo.languages,
project: repo.project,
})
file.isDir = stat.isDirectory()
files.push(file)
}
log('stat=%j', stat, null)
log('processed=%d allPaths.length=%d', processed, allPaths.length)
})
return sort(files).asc(u => u.path)
}
// READY Refactor readFileContent to use async/await
// #esm-migration #important #urgent
// <!--
// order:-40
// -->
repo.readFileContent = async function (file) {
if (!File.isFile(file)) throw new Error(ERRORS.NOT_A_FILE)
var filePath = repo.getFullPath(file)
const stats = await stat(filePath)
if (!stats.isFile()) return file
const data = await readFile(filePath, 'utf8')
file
.setContentFromFile(data)
.setModifiedTime(stats.mtime)
.setCreatedTime(stats.birthtime)
file.lineCount = eol.split(data).length
return file
}
// READY Refactor deleteFile to use async/await
// #esm-migration #important
repo.deleteFile = async function (path) {
const file = File.isFile(path) ? path : repo.getFile(path)
if (!file) throw new Error('Unable to delete:' + path)
try {
await unlink(repo.getFullPath(file))
repo.removeFile(file)
} catch (err) {
throw new Error('Unable to delete file:' + path, {cause: err})
}
}
return repo
}