working!!!! (hopefully)
This commit is contained in:
parent
24a8def394
commit
e6766fff42
3 changed files with 70 additions and 79 deletions
|
|
@ -1,10 +1,6 @@
|
||||||
import type { Logger } from "src/tracing/logger";
|
import type { Logger } from "src/tracing/logger";
|
||||||
import type { FileSystemOperations } from "./filesystem-operations";
|
import type { FileSystemOperations } from "./filesystem-operations";
|
||||||
import type {
|
import type { Database, RelativePath } from "src/persistence/database";
|
||||||
Database,
|
|
||||||
DocumentId,
|
|
||||||
RelativePath
|
|
||||||
} from "src/persistence/database";
|
|
||||||
import { isBinary, isFileTypeMergable, mergeText } from "sync_lib";
|
import { isBinary, isFileTypeMergable, mergeText } from "sync_lib";
|
||||||
import {
|
import {
|
||||||
FileNotFoundError,
|
FileNotFoundError,
|
||||||
|
|
@ -53,12 +49,12 @@ export class FileOperations {
|
||||||
return this.fs.exists(path);
|
return this.fs.exists(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and write the file if it doesn't exist.Otherwise, it has the same behavior as write.
|
// 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.
|
// All parent directories are created if they don't exist.
|
||||||
public async create(
|
public async create(
|
||||||
path: RelativePath,
|
path: RelativePath,
|
||||||
newContent: Uint8Array,
|
newContent: Uint8Array,
|
||||||
documentId?: DocumentId
|
whatevs?: any
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.logger.debug(`Creating file: ${path}`);
|
this.logger.debug(`Creating file: ${path}`);
|
||||||
if (await this.fs.exists(path)) {
|
if (await this.fs.exists(path)) {
|
||||||
|
|
@ -67,25 +63,14 @@ export class FileOperations {
|
||||||
`Didn't expect ${path} to exist, deconflicting by moving it to '${deconflictedPath}'`
|
`Didn't expect ${path} to exist, deconflicting by moving it to '${deconflictedPath}'`
|
||||||
);
|
);
|
||||||
|
|
||||||
const document =
|
// this.database.move(path, deconflictedPath);
|
||||||
this.database.getLatestDocumentByRelativePath(path);
|
|
||||||
this.logger.debug(
|
|
||||||
`Existing metadata for ${path}: ${JSON.stringify(document?.metadata)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (document !== undefined && document.documentId === documentId) {
|
|
||||||
// This can happen if the document got moved both locally and remotely
|
|
||||||
// to the same file path. In this case, we shouldn't deconflict, however,
|
|
||||||
// we also can't overwrite otherwise we'd lose changes.
|
|
||||||
throw new FileNotFoundError(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.database.move(path, deconflictedPath);
|
|
||||||
await this.fs.rename(path, deconflictedPath);
|
await this.fs.rename(path, deconflictedPath);
|
||||||
} else {
|
} else {
|
||||||
await this.createParentDirectories(path);
|
await this.createParentDirectories(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
whatevs?.();
|
||||||
|
|
||||||
await this.fs.write(path, newContent);
|
await this.fs.write(path, newContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,8 +137,7 @@ export class FileOperations {
|
||||||
|
|
||||||
public async move(
|
public async move(
|
||||||
oldPath: RelativePath,
|
oldPath: RelativePath,
|
||||||
newPath: RelativePath,
|
newPath: RelativePath
|
||||||
documentId?: DocumentId
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (oldPath === newPath) {
|
if (oldPath === newPath) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -165,26 +149,14 @@ export class FileOperations {
|
||||||
`Conflict when moving '${oldPath}' to '${newPath}', the latter already exists, deconflicting by moving it to '${deconflictedPath}'`
|
`Conflict when moving '${oldPath}' to '${newPath}', the latter already exists, deconflicting by moving it to '${deconflictedPath}'`
|
||||||
);
|
);
|
||||||
|
|
||||||
const document =
|
// this.database.move(newPath, deconflictedPath);
|
||||||
this.database.getLatestDocumentByRelativePath(newPath);
|
// this.database.move(oldPath, newPath);
|
||||||
|
|
||||||
if (
|
|
||||||
document?.metadata !== undefined &&
|
|
||||||
document.documentId === documentId
|
|
||||||
) {
|
|
||||||
// This can happen if the document got moved both locally and remotely
|
|
||||||
// to the same file path. In this case, we shouldn't deconflict, however,
|
|
||||||
// we also can't overwrite otherwise we'd lose changes.
|
|
||||||
throw new FileNotFoundError(newPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.database.move(newPath, deconflictedPath);
|
|
||||||
await this.fs.rename(newPath, deconflictedPath);
|
await this.fs.rename(newPath, deconflictedPath);
|
||||||
} else {
|
} else {
|
||||||
|
// this.database.move(oldPath, newPath);
|
||||||
await this.createParentDirectories(newPath);
|
await this.createParentDirectories(newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.database.move(oldPath, newPath);
|
|
||||||
await this.fs.rename(oldPath, newPath);
|
await this.fs.rename(oldPath, newPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -226,17 +198,12 @@ export class FileOperations {
|
||||||
);
|
);
|
||||||
stem = stem.replace(FileOperations.PARENTHESES_REGEX, "");
|
stem = stem.replace(FileOperations.PARENTHESES_REGEX, "");
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
let newName;
|
||||||
while (true) {
|
do {
|
||||||
const newName =
|
|
||||||
currentCount === 0
|
|
||||||
? `${directory}${stem}${extension}`
|
|
||||||
: `${directory}${stem} (${currentCount})${extension}`;
|
|
||||||
if (await this.fs.exists(newName)) {
|
|
||||||
currentCount++;
|
currentCount++;
|
||||||
} else {
|
newName = `${directory}${stem} (${currentCount})${extension}`;
|
||||||
|
} while (await this.fs.exists(newName));
|
||||||
|
|
||||||
return newName;
|
return newName;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,42 +36,44 @@ export class UnrestrictedSyncer {
|
||||||
proposedDocumentId: DocumentId,
|
proposedDocumentId: DocumentId,
|
||||||
getLatestDocument: () => DocumentRecord
|
getLatestDocument: () => DocumentRecord
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let latestDocument = getLatestDocument();
|
let document = getLatestDocument();
|
||||||
|
|
||||||
return this.executeSync(
|
return this.executeSync(
|
||||||
[latestDocument.relativePath],
|
[document.relativePath],
|
||||||
SyncType.CREATE,
|
SyncType.CREATE,
|
||||||
SyncSource.PUSH,
|
SyncSource.PUSH,
|
||||||
async () => {
|
async () => {
|
||||||
|
document = getLatestDocument();
|
||||||
|
|
||||||
const contentBytes = await this.operations.read(
|
const contentBytes = await this.operations.read(
|
||||||
latestDocument.relativePath
|
document.relativePath
|
||||||
); // this can throw FileNotFoundError
|
); // this can throw FileNotFoundError
|
||||||
const contentHash = hash(contentBytes);
|
const contentHash = hash(contentBytes);
|
||||||
|
|
||||||
const response = await this.syncService.create({
|
const response = await this.syncService.create({
|
||||||
documentId: proposedDocumentId,
|
documentId: proposedDocumentId,
|
||||||
relativePath: latestDocument.relativePath,
|
relativePath: document.relativePath,
|
||||||
contentBytes
|
contentBytes
|
||||||
});
|
});
|
||||||
|
|
||||||
latestDocument = getLatestDocument();
|
document = getLatestDocument();
|
||||||
|
|
||||||
this.history.addHistoryEntry({
|
this.history.addHistoryEntry({
|
||||||
status: SyncStatus.SUCCESS,
|
status: SyncStatus.SUCCESS,
|
||||||
source: SyncSource.PUSH,
|
source: SyncSource.PUSH,
|
||||||
relativePath: latestDocument.relativePath,
|
relativePath: document.relativePath,
|
||||||
message: `Successfully uploaded locally created file`,
|
message: `Successfully uploaded locally created file`,
|
||||||
type: SyncType.CREATE
|
type: SyncType.CREATE
|
||||||
});
|
});
|
||||||
|
|
||||||
this.database.setDocument(
|
this.database.setDocument(
|
||||||
{
|
{
|
||||||
relativePath: latestDocument.relativePath,
|
relativePath: document.relativePath,
|
||||||
documentId: response.documentId,
|
documentId: response.documentId,
|
||||||
parentVersionId: response.vaultUpdateId,
|
parentVersionId: response.vaultUpdateId,
|
||||||
hash: contentHash
|
hash: contentHash
|
||||||
},
|
},
|
||||||
latestDocument.identity
|
document.identity
|
||||||
);
|
);
|
||||||
|
|
||||||
this.tryIncrementVaultUpdateId(response.vaultUpdateId);
|
this.tryIncrementVaultUpdateId(response.vaultUpdateId);
|
||||||
|
|
@ -88,6 +90,8 @@ export class UnrestrictedSyncer {
|
||||||
SyncType.DELETE,
|
SyncType.DELETE,
|
||||||
SyncSource.PUSH,
|
SyncSource.PUSH,
|
||||||
async () => {
|
async () => {
|
||||||
|
document = getLatestDocument();
|
||||||
|
|
||||||
const response = await this.syncService.delete({
|
const response = await this.syncService.delete({
|
||||||
documentId: document.documentId,
|
documentId: document.documentId,
|
||||||
relativePath: document.relativePath
|
relativePath: document.relativePath
|
||||||
|
|
@ -132,6 +136,9 @@ export class UnrestrictedSyncer {
|
||||||
SyncType.UPDATE,
|
SyncType.UPDATE,
|
||||||
SyncSource.PUSH,
|
SyncSource.PUSH,
|
||||||
async () => {
|
async () => {
|
||||||
|
document = getLatestDocument();
|
||||||
|
const originalRelativePath = document.relativePath;
|
||||||
|
|
||||||
if (document.metadata === undefined || document.isDeleted) {
|
if (document.metadata === undefined || document.isDeleted) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Document ${document.relativePath} has been already deleted, no need to update it`
|
`Document ${document.relativePath} has been already deleted, no need to update it`
|
||||||
|
|
@ -194,8 +201,6 @@ export class UnrestrictedSyncer {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.isDeleted) {
|
if (response.isDeleted) {
|
||||||
await this.operations.delete(document.relativePath);
|
|
||||||
|
|
||||||
this.history.addHistoryEntry({
|
this.history.addHistoryEntry({
|
||||||
status: SyncStatus.SUCCESS,
|
status: SyncStatus.SUCCESS,
|
||||||
source: SyncSource.PULL,
|
source: SyncSource.PULL,
|
||||||
|
|
@ -216,21 +221,25 @@ export class UnrestrictedSyncer {
|
||||||
document.identity
|
document.identity
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.operations.delete(document.relativePath);
|
||||||
|
|
||||||
this.tryIncrementVaultUpdateId(response.vaultUpdateId);
|
this.tryIncrementVaultUpdateId(response.vaultUpdateId);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.relativePath != document.relativePath) {
|
let actualPath = document.relativePath;
|
||||||
|
|
||||||
|
if (response.relativePath != originalRelativePath) {
|
||||||
// this.database.getNewResolvedDocumentByRelativePath(
|
// this.database.getNewResolvedDocumentByRelativePath(
|
||||||
// response.relativePath,
|
// response.relativePath,
|
||||||
// promise
|
// promise
|
||||||
// );
|
// );
|
||||||
|
|
||||||
|
actualPath = response.relativePath;
|
||||||
await this.operations.move(
|
await this.operations.move(
|
||||||
document.relativePath,
|
document.relativePath,
|
||||||
response.relativePath,
|
response.relativePath
|
||||||
response.documentId
|
|
||||||
); // this can throw FileNotFoundError
|
); // this can throw FileNotFoundError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,7 +248,7 @@ export class UnrestrictedSyncer {
|
||||||
contentHash = hash(responseBytes);
|
contentHash = hash(responseBytes);
|
||||||
|
|
||||||
await this.operations.write(
|
await this.operations.write(
|
||||||
response.relativePath,
|
actualPath,
|
||||||
contentBytes,
|
contentBytes,
|
||||||
responseBytes
|
responseBytes
|
||||||
);
|
);
|
||||||
|
|
@ -253,12 +262,10 @@ export class UnrestrictedSyncer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
document = getLatestDocument();
|
|
||||||
|
|
||||||
this.database.setDocument(
|
this.database.setDocument(
|
||||||
{
|
{
|
||||||
documentId: response.documentId,
|
documentId: response.documentId,
|
||||||
relativePath: document.relativePath,
|
relativePath: actualPath,
|
||||||
parentVersionId: response.vaultUpdateId,
|
parentVersionId: response.vaultUpdateId,
|
||||||
hash: contentHash
|
hash: contentHash
|
||||||
},
|
},
|
||||||
|
|
@ -326,6 +333,7 @@ export class UnrestrictedSyncer {
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
localMetadata?.metadata?.parentVersionId ??
|
localMetadata?.metadata?.parentVersionId ??
|
||||||
-1 >= remoteVersion.vaultUpdateId
|
-1 >= remoteVersion.vaultUpdateId
|
||||||
|
|
@ -338,6 +346,21 @@ export class UnrestrictedSyncer {
|
||||||
|
|
||||||
const contentBytes = deserialize(content);
|
const contentBytes = deserialize(content);
|
||||||
|
|
||||||
|
const [promise, resolve] = createPromise();
|
||||||
|
|
||||||
|
await this.operations.create(
|
||||||
|
remoteVersion.relativePath,
|
||||||
|
contentBytes,
|
||||||
|
() =>
|
||||||
|
this.database.getNewResolvedDocumentByRelativePath(
|
||||||
|
remoteVersion.documentId,
|
||||||
|
remoteVersion.relativePath,
|
||||||
|
promise
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const document =
|
||||||
|
this.database.getDocumentByUpdatePromise(promise);
|
||||||
this.database.setDocument(
|
this.database.setDocument(
|
||||||
{
|
{
|
||||||
documentId: remoteVersion.documentId,
|
documentId: remoteVersion.documentId,
|
||||||
|
|
@ -345,14 +368,10 @@ export class UnrestrictedSyncer {
|
||||||
parentVersionId: remoteVersion.vaultUpdateId,
|
parentVersionId: remoteVersion.vaultUpdateId,
|
||||||
hash: hash(contentBytes)
|
hash: hash(contentBytes)
|
||||||
},
|
},
|
||||||
localMetadata?.identity
|
document.identity
|
||||||
);
|
|
||||||
|
|
||||||
await this.operations.create(
|
|
||||||
remoteVersion.relativePath,
|
|
||||||
contentBytes,
|
|
||||||
remoteVersion.documentId
|
|
||||||
);
|
);
|
||||||
|
resolve();
|
||||||
|
this.database.removeDocumentPromise(promise);
|
||||||
|
|
||||||
this.history.addHistoryEntry({
|
this.history.addHistoryEntry({
|
||||||
status: SyncStatus.SUCCESS,
|
status: SyncStatus.SUCCESS,
|
||||||
|
|
|
||||||
|
|
@ -82,11 +82,11 @@ export class MockClient implements FileSystemOperations {
|
||||||
const newContentUint8Array = new TextEncoder().encode(newContent);
|
const newContentUint8Array = new TextEncoder().encode(newContent);
|
||||||
this.localFiles.set(path, newContentUint8Array);
|
this.localFiles.set(path, newContentUint8Array);
|
||||||
|
|
||||||
const existingPats = currentContent
|
const existingParts = currentContent
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((part) => part.trim());
|
.map((part) => part.trim());
|
||||||
const newParts = newContent.split(" ").map((part) => part.trim());
|
const newParts = newContent.split(" ").map((part) => part.trim());
|
||||||
existingPats.forEach((part) =>
|
existingParts.forEach((part) =>
|
||||||
// all changes should be additive
|
// all changes should be additive
|
||||||
assert(
|
assert(
|
||||||
newParts.includes(part),
|
newParts.includes(part),
|
||||||
|
|
@ -106,15 +106,20 @@ export class MockClient implements FileSystemOperations {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async write(path: RelativePath, content: Uint8Array): Promise<void> {
|
public async write(path: RelativePath, content: Uint8Array): Promise<void> {
|
||||||
|
const hasExisted = this.localFiles.has(path);
|
||||||
this.localFiles.set(path, content);
|
this.localFiles.set(path, content);
|
||||||
|
|
||||||
this.client.logger.info(
|
this.client.logger.info(
|
||||||
`Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}`
|
`Updated file ${path} with:\n new content: ${new TextDecoder().decode(content)}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (hasExisted) {
|
||||||
void this.client.syncer.syncLocallyUpdatedFile({
|
void this.client.syncer.syncLocallyUpdatedFile({
|
||||||
relativePath: path
|
relativePath: path
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
void this.client.syncer.syncLocallyCreatedFile(path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(path: RelativePath): Promise<void> {
|
public async delete(path: RelativePath): Promise<void> {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue