mirror of https://github.com/ghostfolio/ghostfolio
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
309 lines
11 KiB
309 lines
11 KiB
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
exports.FsTree = void 0;
|
|
exports.flushChanges = flushChanges;
|
|
exports.printChanges = printChanges;
|
|
const node_fs_1 = require("node:fs");
|
|
const logger_1 = require("../utils/logger");
|
|
const output_1 = require("../utils/output");
|
|
const path_1 = require("path");
|
|
const chalk = require("chalk");
|
|
class FsTree {
|
|
constructor(root, isVerbose, logOperationId) {
|
|
this.root = root;
|
|
this.isVerbose = isVerbose;
|
|
this.logOperationId = logOperationId;
|
|
this.recordedChanges = {};
|
|
/**
|
|
* Signifies if operations on the tree instance
|
|
* are allowed. Set to false after changes are written
|
|
* to disk, to prevent someone trying to use the tree to update
|
|
* files when the tree is no longer effective.
|
|
*/
|
|
this.locked = false;
|
|
}
|
|
read(filePath, encoding) {
|
|
filePath = this.normalize(filePath);
|
|
try {
|
|
let content;
|
|
if (this.recordedChanges[this.rp(filePath)]) {
|
|
content = this.recordedChanges[this.rp(filePath)].content;
|
|
}
|
|
else {
|
|
content = this.fsReadFile(filePath);
|
|
}
|
|
return encoding ? content.toString(encoding) : content;
|
|
}
|
|
catch (e) {
|
|
if (this.isVerbose) {
|
|
logger_1.logger.error(e);
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
write(filePath, content, options) {
|
|
this.assertUnlocked();
|
|
filePath = this.normalize(filePath);
|
|
// Remove any recorded changes where a parent directory has been
|
|
// deleted when writing a new file within the directory.
|
|
let parent = (0, path_1.dirname)(this.rp(filePath));
|
|
while (parent !== '.') {
|
|
if (this.recordedChanges[parent]?.isDeleted) {
|
|
delete this.recordedChanges[parent];
|
|
}
|
|
parent = (0, path_1.dirname)(parent);
|
|
}
|
|
if (this.fsExists(this.rp(filePath)) &&
|
|
Buffer.from(content).equals(this.fsReadFile(filePath))) {
|
|
// Remove recorded change because the file has been restored to it's original contents
|
|
delete this.recordedChanges[this.rp(filePath)];
|
|
return;
|
|
}
|
|
try {
|
|
this.recordedChanges[this.rp(filePath)] = {
|
|
content: Buffer.from(content),
|
|
isDeleted: false,
|
|
options,
|
|
};
|
|
}
|
|
catch (e) {
|
|
if (this.isVerbose) {
|
|
logger_1.logger.error(e);
|
|
}
|
|
}
|
|
}
|
|
overwrite(filePath, content, options) {
|
|
filePath = this.normalize(filePath);
|
|
this.write(filePath, content, options);
|
|
}
|
|
delete(filePath) {
|
|
this.assertUnlocked();
|
|
filePath = this.normalize(filePath);
|
|
if (this.filesForDir(this.rp(filePath)).length > 0) {
|
|
this.filesForDir(this.rp(filePath)).forEach((f) => (this.recordedChanges[f] = { content: null, isDeleted: true }));
|
|
}
|
|
this.recordedChanges[this.rp(filePath)] = {
|
|
content: null,
|
|
isDeleted: true,
|
|
};
|
|
// Delete directory when is not root and there are no children
|
|
if (filePath !== '' &&
|
|
this.children((0, path_1.dirname)(this.rp(filePath))).length < 1) {
|
|
this.delete((0, path_1.dirname)(this.rp(filePath)));
|
|
}
|
|
}
|
|
exists(filePath) {
|
|
filePath = this.normalize(filePath);
|
|
try {
|
|
if (this.recordedChanges[this.rp(filePath)]) {
|
|
return !this.recordedChanges[this.rp(filePath)].isDeleted;
|
|
}
|
|
else if (this.filesForDir(this.rp(filePath)).length > 0) {
|
|
return true;
|
|
}
|
|
else {
|
|
return this.fsExists(filePath);
|
|
}
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
rename(from, to) {
|
|
this.assertUnlocked();
|
|
from = this.normalize(from);
|
|
to = this.normalize(to);
|
|
if (from === to) {
|
|
return;
|
|
}
|
|
if (this.isFile(from)) {
|
|
const content = this.read(this.rp(from));
|
|
this.write(this.rp(to), content);
|
|
this.delete(this.rp(from));
|
|
}
|
|
else {
|
|
for (const child of this.children(from)) {
|
|
this.rename((0, path_1.join)(from, child), (0, path_1.join)(to, child));
|
|
}
|
|
}
|
|
}
|
|
isFile(filePath) {
|
|
filePath = this.normalize(filePath);
|
|
try {
|
|
if (this.recordedChanges[this.rp(filePath)]) {
|
|
return !this.recordedChanges[this.rp(filePath)].isDeleted;
|
|
}
|
|
else {
|
|
return this.fsIsFile(filePath);
|
|
}
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
children(dirPath) {
|
|
dirPath = this.normalize(dirPath);
|
|
let res = this.fsReadDir(dirPath);
|
|
res = [...res, ...this.directChildrenOfDir(this.rp(dirPath))];
|
|
res = res.filter((q) => {
|
|
const r = this.recordedChanges[this.normalize((0, path_1.join)(this.rp(dirPath), q))];
|
|
return !r?.isDeleted;
|
|
});
|
|
// Dedupe
|
|
return Array.from(new Set(res));
|
|
}
|
|
listChanges() {
|
|
const res = [];
|
|
Object.keys(this.recordedChanges).forEach((f) => {
|
|
if (this.recordedChanges[f].isDeleted) {
|
|
if (this.fsExists(f)) {
|
|
res.push({ path: f, type: 'DELETE', content: null });
|
|
}
|
|
}
|
|
else {
|
|
if (this.fsExists(f)) {
|
|
res.push({
|
|
path: f,
|
|
type: 'UPDATE',
|
|
content: this.recordedChanges[f].content,
|
|
options: this.recordedChanges[f].options,
|
|
});
|
|
}
|
|
else {
|
|
res.push({
|
|
path: f,
|
|
type: 'CREATE',
|
|
content: this.recordedChanges[f].content,
|
|
options: this.recordedChanges[f].options,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
return res;
|
|
}
|
|
changePermissions(filePath, mode) {
|
|
this.assertUnlocked();
|
|
filePath = this.normalize(filePath);
|
|
const filePathChangeKey = this.rp(filePath);
|
|
if (this.recordedChanges[filePathChangeKey]) {
|
|
if (this.recordedChanges[filePathChangeKey].isDeleted) {
|
|
throw new Error(`Cannot change permissions of deleted file ${filePath}.`);
|
|
}
|
|
this.recordedChanges[filePathChangeKey].options = { mode };
|
|
}
|
|
else if (!this.fsExists(filePath)) {
|
|
throw new Error(`Cannot change permissions of non-existing file ${filePath}.`);
|
|
}
|
|
else if (!this.fsIsFile(filePath)) {
|
|
// To fully support directories we'd need to change how we store
|
|
// changes to keep a record of directories so we can associate
|
|
// permissions to them.
|
|
throw new Error(`Cannot change permissions of non-file ${filePath}.`);
|
|
}
|
|
else {
|
|
this.recordedChanges[filePathChangeKey] = {
|
|
content: this.fsReadFile(filePath),
|
|
isDeleted: false,
|
|
options: { mode },
|
|
};
|
|
}
|
|
}
|
|
// Marks FsTree as final.
|
|
lock() {
|
|
this.locked = true;
|
|
}
|
|
assertUnlocked() {
|
|
if (this.locked) {
|
|
output_1.output.error({
|
|
title: `File changes have already been written to disk. Further changes were attempted ${this.logOperationId ? ` while running ${this.logOperationId}.` : '.'}`,
|
|
bodyLines: [
|
|
'The file system can no longer be modified. This commonly happens when a generator attempts to make further changes in its callback, or an asynchronous operation is still running after the generator completes.',
|
|
],
|
|
});
|
|
throw new Error('Tree changed after commit to disk.');
|
|
}
|
|
}
|
|
normalize(path) {
|
|
return (0, path_1.relative)(this.root, (0, path_1.join)(this.root, path)).split(path_1.sep).join('/');
|
|
}
|
|
fsReadDir(dirPath) {
|
|
try {
|
|
return (0, node_fs_1.readdirSync)((0, path_1.join)(this.root, dirPath));
|
|
}
|
|
catch {
|
|
return [];
|
|
}
|
|
}
|
|
fsIsFile(filePath) {
|
|
const stat = (0, node_fs_1.statSync)((0, path_1.join)(this.root, filePath));
|
|
return stat.isFile();
|
|
}
|
|
fsReadFile(filePath) {
|
|
return (0, node_fs_1.readFileSync)((0, path_1.join)(this.root, filePath));
|
|
}
|
|
fsExists(filePath) {
|
|
try {
|
|
const stat = (0, node_fs_1.statSync)((0, path_1.join)(this.root, filePath));
|
|
return stat.isFile() || stat.isDirectory();
|
|
}
|
|
catch {
|
|
return false;
|
|
}
|
|
}
|
|
filesForDir(path) {
|
|
return Object.keys(this.recordedChanges).filter((f) => f.startsWith(`${path}/`) && !this.recordedChanges[f].isDeleted);
|
|
}
|
|
directChildrenOfDir(path) {
|
|
const res = {};
|
|
if (path === '') {
|
|
return Object.keys(this.recordedChanges).map((file) => file.split('/')[0]);
|
|
}
|
|
Object.keys(this.recordedChanges).forEach((f) => {
|
|
if (f.startsWith(`${path}/`)) {
|
|
// Remove the current folder's path from the directory
|
|
const file = f.substring(path.length + 1);
|
|
// Split the path on segments, and take the first one
|
|
const basePath = file.split('/')[0];
|
|
// Mark it as a child of the current directory
|
|
res[basePath] = true;
|
|
}
|
|
});
|
|
return Object.keys(res);
|
|
}
|
|
rp(pp) {
|
|
return pp.startsWith('/') ? pp.substring(1) : pp;
|
|
}
|
|
}
|
|
exports.FsTree = FsTree;
|
|
function flushChanges(root, fileChanges) {
|
|
fileChanges.forEach((f) => {
|
|
const fpath = (0, path_1.join)(root, f.path);
|
|
if (f.type === 'CREATE') {
|
|
(0, node_fs_1.mkdirSync)((0, path_1.dirname)(fpath), { recursive: true });
|
|
(0, node_fs_1.writeFileSync)(fpath, f.content);
|
|
if (f.options?.mode)
|
|
(0, node_fs_1.chmodSync)(fpath, f.options.mode);
|
|
}
|
|
else if (f.type === 'UPDATE') {
|
|
(0, node_fs_1.writeFileSync)(fpath, f.content);
|
|
if (f.options?.mode)
|
|
(0, node_fs_1.chmodSync)(fpath, f.options.mode);
|
|
}
|
|
else if (f.type === 'DELETE') {
|
|
(0, node_fs_1.rmSync)(fpath, { recursive: true, force: true });
|
|
}
|
|
});
|
|
}
|
|
function printChanges(fileChanges, indent = '') {
|
|
fileChanges.forEach((f) => {
|
|
if (f.type === 'CREATE') {
|
|
console.log(`${indent}${chalk.green('CREATE')} ${f.path}`);
|
|
}
|
|
else if (f.type === 'UPDATE') {
|
|
console.log(`${indent}${chalk.white('UPDATE')} ${f.path}`);
|
|
}
|
|
else if (f.type === 'DELETE') {
|
|
console.log(`${indent}${chalk.yellow('DELETE')} ${f.path}`);
|
|
}
|
|
});
|
|
}
|
|
|