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

@ -14,10 +14,12 @@ export class ObsidianFileSystemOperations implements FileSystemOperations {
private readonly workspace: Workspace
) {}
public async listAllFiles(): Promise<RelativePath[]> {
public async listFilesRecursively(
root: RelativePath | undefined
): Promise<RelativePath[]> {
// Let's implement this by hand because vault.adapter.listAllFiles doesn't always return all files.
const allFiles = [];
const remainingFolders = [this.vault.getRoot().path];
const remainingFolders = [root ?? this.vault.getRoot().path];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
while (true) {

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())
]);

View file

@ -94,7 +94,7 @@ export class MockAgent extends MockClient {
options.push(this.enableSyncAction.bind(this));
}
const files = await this.listAllFiles();
const files = await this.listFilesRecursively();
if (files.length > 0) {
options.push(

View file

@ -7,6 +7,7 @@ import {
SyncClient
} from "sync-client";
import type { TextWithCursors } from "reconcile-text";
export class MockClient implements FileSystemOperations {
protected readonly localFiles = new Map<string, Uint8Array>();
protected client!: SyncClient;
@ -46,7 +47,9 @@ export class MockClient implements FileSystemOperations {
await this.client.start();
}
public async listAllFiles(): Promise<RelativePath[]> {
public async listFilesRecursively(
_root: RelativePath | undefined = undefined // we don't use multi-level paths during tests
): Promise<RelativePath[]> {
return Array.from(this.localFiles.keys());
}

View file

@ -8,7 +8,6 @@ RUN apt update && \
pkg-config && \
cargo install sqlx-cli
# Build application
COPY . .
RUN sqlx database create --database-url sqlite://db.sqlite3 && \
@ -28,9 +27,6 @@ RUN apt update && \
rm -rf /var/lib/apt/lists/*
COPY --from=builder /usr/src/backend/target/release/sync_server /app/sync_server
COPY test-entrypoint.sh /app/test-entrypoint.sh
RUN chmod +x /app/test-entrypoint.sh
VOLUME /data
EXPOSE 3000/tcp