Fix folder deletion (#140)

This commit is contained in:
Andras Schmelczer 2025-10-20 20:24:35 +01:00 committed by GitHub
parent aa73a5d718
commit 1ddba47b80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 69 additions and 25 deletions

View file

@ -29,8 +29,10 @@ class MockDatabase implements Partial<Database> {
class FakeFileSystemOperations implements FileSystemOperations {
public readonly names = new Set<string>();
public async listAllFiles(): Promise<RelativePath[]> {
throw new Error("Method not implemented.");
public async listFilesRecursively(
_root: RelativePath | undefined
): Promise<RelativePath[]> {
return ["file.md"];
}
public async read(_path: RelativePath): Promise<Uint8Array> {
throw new Error("Method not implemented.");

View file

@ -5,6 +5,7 @@ import { SafeFileSystemOperations } from "./safe-filesystem-operations";
import type { TextWithCursors } from "reconcile-text";
import { isBinary, reconcile } from "reconcile-text";
import { isFileTypeMergable } from "../utils/is-file-type-mergable";
export class FileOperations {
private static readonly PARENTHESES_REGEX = / \((\d+)\)$/;
private readonly fs: SafeFileSystemOperations;
@ -18,8 +19,22 @@ export class FileOperations {
this.fs = new SafeFileSystemOperations(fs, logger);
}
public async listAllFiles(): Promise<RelativePath[]> {
return this.fs.listAllFiles();
private static getParentDirAndFile(
path: RelativePath
): [RelativePath, RelativePath] {
const pathParts = path.split("/");
const fileName = pathParts.pop();
if (fileName == "" || fileName == null) {
throw new Error(`Path '${path}' cannot be empty`);
}
return [pathParts.join("/"), fileName];
}
public async listFilesRecursively(
root: RelativePath | undefined = undefined
): Promise<RelativePath[]> {
return this.fs.listFilesRecursively(root);
}
public async read(path: RelativePath): Promise<Uint8Array> {
@ -120,7 +135,8 @@ export class FileOperations {
public async delete(path: RelativePath): Promise<void> {
if (await this.exists(path)) {
return this.fs.delete(path);
await this.fs.delete(path);
await this.deletingEmptyParentDirectoriesOfDeletedFile(path);
} else {
this.logger.debug(`No need to delete '${path}', it doesn't exist`);
}
@ -146,6 +162,31 @@ export class FileOperations {
this.database.move(oldPath, newPath);
await this.fs.rename(oldPath, newPath);
await this.deletingEmptyParentDirectoriesOfDeletedFile(oldPath);
}
private async deletingEmptyParentDirectoriesOfDeletedFile(
path: RelativePath
): Promise<void> {
let directory = path;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {
[directory] = FileOperations.getParentDirAndFile(directory);
if (directory.length === 0) {
break;
}
const remainingContent =
await this.fs.listFilesRecursively(directory);
if (remainingContent.length === 0) {
this.logger.debug(
`Folder (${directory}) is now empty, deleting`
);
await this.fs.delete(directory);
} else {
break;
}
}
}
private fromNativeLineEndings(content: Uint8Array): Uint8Array {
@ -184,13 +225,9 @@ export class FileOperations {
}
private async deconflictPath(path: RelativePath): Promise<RelativePath> {
const pathParts = path.split("/");
const fileName = pathParts.pop();
if (fileName == "" || fileName == null) {
throw new Error(`Path '${path}' cannot be empty`);
}
// eslint-disable-next-line prefer-const
let [directory, fileName] = FileOperations.getParentDirAndFile(path);
let directory = pathParts.join("/");
if (directory) {
directory += "/";
}

View file

@ -3,8 +3,10 @@ import type { RelativePath } from "../persistence/database";
import type { TextWithCursors } from "reconcile-text";
export interface FileSystemOperations {
// List all files that should be synced.
listAllFiles: () => Promise<RelativePath[]>;
// List all files under root that should be synced. If root is undefined, return every file.
listFilesRecursively: (
root: RelativePath | undefined
) => Promise<RelativePath[]>;
// Read the content of a file.
read: (path: RelativePath) => Promise<Uint8Array>;

View file

@ -20,9 +20,11 @@ export class SafeFileSystemOperations implements FileSystemOperations {
this.locks = new Locks(logger);
}
public async listAllFiles(): Promise<RelativePath[]> {
public async listFilesRecursively(
root: RelativePath | undefined
): Promise<RelativePath[]> {
this.logger.debug("Listing all files");
const result = await this.fs.listAllFiles();
const result = await this.fs.listFilesRecursively(root);
this.logger.debug(`Listed ${result.length} files`);
return result;
}

View file

@ -335,7 +335,7 @@ export class Syncer {
private async internalScheduleSyncForOfflineChanges(): Promise<void> {
await this.createFakeDocumentsFromRemoteState();
const allLocalFiles = await this.operations.listAllFiles();
const allLocalFiles = await this.operations.listFilesRecursively();
let locallyPossiblyDeletedFiles: DocumentRecord[] = [];
@ -431,7 +431,7 @@ export class Syncer {
}
const [allLocalFiles, remote] = await Promise.all([
this.operations.listAllFiles(),
this.operations.listFilesRecursively(),
this.syncQueue.add(async () => this.syncService.getAll())
]);