Add lineending support and clean up
This commit is contained in:
parent
1c904909af
commit
79eb4f6c7b
1 changed files with 69 additions and 58 deletions
|
|
@ -11,50 +11,32 @@ export class FileOperations {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly logger: Logger,
|
private readonly logger: Logger,
|
||||||
private readonly database: Database,
|
private readonly database: Database,
|
||||||
fs: FileSystemOperations
|
fs: FileSystemOperations,
|
||||||
|
private readonly nativeLineEndings: string = "\n"
|
||||||
) {
|
) {
|
||||||
this.fs = new SafeFileSystemOperations(fs, logger);
|
this.fs = new SafeFileSystemOperations(fs, logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async listAllFiles(): Promise<RelativePath[]> {
|
public async listAllFiles(): Promise<RelativePath[]> {
|
||||||
const files = await this.fs.listAllFiles();
|
return this.fs.listAllFiles();
|
||||||
this.logger.debug(`Listing all files, found ${files.length}`);
|
|
||||||
return files;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async read(path: RelativePath): Promise<Uint8Array> {
|
public async read(path: RelativePath): Promise<Uint8Array> {
|
||||||
const content = await this.fs.read(path);
|
return this.fromNativeLineEndings(await this.fs.read(path));
|
||||||
|
|
||||||
if (isBinary(content)) {
|
|
||||||
return content;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decoder = new TextDecoder("utf-8");
|
/**
|
||||||
|
* Create a file at the specified path.
|
||||||
// Normalize line-endings to LF on Windows
|
*
|
||||||
let text = decoder.decode(content);
|
* If a file with the same name already exists, it is moved before creating the new one.
|
||||||
text = text.replace(/\r\n/g, "\n");
|
* Parent directories are created if necessary.
|
||||||
|
*/
|
||||||
return new TextEncoder().encode(text);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getFileSize(path: RelativePath): Promise<number> {
|
|
||||||
return this.fs.getFileSize(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async exists(path: RelativePath): Promise<boolean> {
|
|
||||||
return this.fs.exists(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create and write the file if it doesn't exist. Otherwise, it has the same behavior as write.
|
|
||||||
// All parent directories are created if they don't exist.
|
|
||||||
public async create(
|
public async create(
|
||||||
path: RelativePath,
|
path: RelativePath,
|
||||||
newContent: Uint8Array
|
newContent: Uint8Array
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.debug(`Creating file: ${path}`);
|
await this.ensureClearPath(path);
|
||||||
|
return this.fs.write(path, this.toNativeLineEndings(newContent));
|
||||||
await this.fs.write(path, newContent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ensureClearPath(path: RelativePath): Promise<void> {
|
public async ensureClearPath(path: RelativePath): Promise<void> {
|
||||||
|
|
@ -71,19 +53,22 @@ export class FileOperations {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the file at the given path.
|
/**
|
||||||
// If the file's content is different from `expectedContent`, the a 3-way merge is performed before writing.
|
* Update the file at the given path.
|
||||||
// If the file no longer exists, the file is not recreated and an empty array is returned.
|
*
|
||||||
|
* Performs a 3-way merge before writing if the file's content differs from `expectedContent`.
|
||||||
|
* Does not recreate the file if it no longer exists, returning an empty array instead.
|
||||||
|
*/
|
||||||
public async write(
|
public async write(
|
||||||
path: RelativePath,
|
path: RelativePath,
|
||||||
expectedContent: Uint8Array,
|
expectedContent: Uint8Array,
|
||||||
newContent: Uint8Array
|
newContent: Uint8Array
|
||||||
): Promise<Uint8Array> {
|
): Promise<void> {
|
||||||
if (!(await this.fs.exists(path))) {
|
if (!(await this.fs.exists(path))) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`The caller assumed ${path} exists, but it no longer, so we wont recreate it`
|
`The caller assumed ${path} exists, but it no longer, so we wont recreate it`
|
||||||
);
|
);
|
||||||
return new Uint8Array(0);
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|
@ -94,44 +79,47 @@ export class FileOperations {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`The expected content is not mergable, so we won't perform a 3-way merge, just overwrite it`
|
`The expected content is not mergable, so we won't perform a 3-way merge, just overwrite it`
|
||||||
);
|
);
|
||||||
await this.fs.write(path, newContent);
|
await this.fs.write(
|
||||||
return newContent;
|
path,
|
||||||
|
// `newContent` might not be binary so we still have to ensure the line endings are correct
|
||||||
|
this.toNativeLineEndings(newContent)
|
||||||
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const expectedText = new TextDecoder().decode(expectedContent);
|
const expectedText = new TextDecoder().decode(expectedContent); // this comes from a previous read which must only have \n line endings
|
||||||
const newText = new TextDecoder().decode(newContent);
|
const newText = new TextDecoder().decode(newContent); // this comes from the server which stores text with \n line endings
|
||||||
|
|
||||||
|
await this.fs.atomicUpdateText(path, (currentText) => {
|
||||||
|
currentText = currentText.replace(this.nativeLineEndings, "\n");
|
||||||
|
|
||||||
const resultText = await this.fs.atomicUpdateText(
|
|
||||||
path,
|
|
||||||
(currentText) => {
|
|
||||||
currentText = currentText.replace(/\r\n/g, "\n");
|
|
||||||
if (currentText !== expectedText) {
|
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Performing a 3-way merge for ${path} with the expected content`
|
`Performing a 3-way merge for ${path} with the expected content`
|
||||||
);
|
);
|
||||||
|
|
||||||
return mergeText(expectedText, currentText, newText);
|
return mergeText(expectedText, currentText, newText).replace(
|
||||||
}
|
"\n",
|
||||||
|
this.nativeLineEndings
|
||||||
this.logger.debug(
|
|
||||||
`The current content of ${path} is the same as the expected content, so we will just write the new content`
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
return newText;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return new TextEncoder().encode(resultText);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(path: RelativePath): Promise<void> {
|
public async delete(path: RelativePath): Promise<void> {
|
||||||
if (await this.exists(path)) {
|
if (await this.exists(path)) {
|
||||||
this.logger.debug(`Deleting file: ${path}`);
|
|
||||||
return this.fs.delete(path);
|
return this.fs.delete(path);
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(`No need to delete '${path}', it doesn't exist`);
|
this.logger.debug(`No need to delete '${path}', it doesn't exist`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getFileSize(path: RelativePath): Promise<number> {
|
||||||
|
return this.fs.getFileSize(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async exists(path: RelativePath): Promise<boolean> {
|
||||||
|
return this.fs.exists(path);
|
||||||
|
}
|
||||||
|
|
||||||
public async move(
|
public async move(
|
||||||
oldPath: RelativePath,
|
oldPath: RelativePath,
|
||||||
newPath: RelativePath
|
newPath: RelativePath
|
||||||
|
|
@ -139,12 +127,35 @@ export class FileOperations {
|
||||||
if (oldPath === newPath) {
|
if (oldPath === newPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.ensureClearPath(newPath);
|
await this.ensureClearPath(newPath);
|
||||||
|
|
||||||
this.database.move(oldPath, newPath);
|
this.database.move(oldPath, newPath);
|
||||||
await this.fs.rename(oldPath, newPath);
|
await this.fs.rename(oldPath, newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fromNativeLineEndings(content: Uint8Array): Uint8Array {
|
||||||
|
if (isBinary(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
let text = decoder.decode(content);
|
||||||
|
text = text.replace(this.nativeLineEndings, "\n");
|
||||||
|
return new TextEncoder().encode(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toNativeLineEndings(content: Uint8Array): Uint8Array {
|
||||||
|
if (isBinary(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
let text = decoder.decode(content);
|
||||||
|
text = text.replace("\n", this.nativeLineEndings);
|
||||||
|
return new TextEncoder().encode(text);
|
||||||
|
}
|
||||||
|
|
||||||
private async createParentDirectories(path: string): Promise<void> {
|
private async createParentDirectories(path: string): Promise<void> {
|
||||||
const components = path.split("/");
|
const components = path.split("/");
|
||||||
if (components.length === 1) {
|
if (components.length === 1) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue