2024-04-25 14:31:48 +02:00
|
|
|
import JoplinError from './JoplinError';
|
|
|
|
|
|
|
|
import { DeltaOptions, GetOptions, ItemStat, PutOptions, basicDelta } from './file-api';
|
|
|
|
import FsDriverBase, { Stat } from './fs-driver-base';
|
2017-06-11 23:11:14 +02:00
|
|
|
|
2017-07-24 20:58:11 +02:00
|
|
|
// NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance).
|
|
|
|
// What it means is that if, for example, client 1 changes a note at time t, and client 2 changes the same note within the same second,
|
|
|
|
// both clients will not know about each others updates during the next sync. They will simply both sync their note and whoever
|
|
|
|
// comes last will overwrite (on the remote storage) the note of the other client. Both client will then have a different note at
|
|
|
|
// that point and that will only be resolved if one of them changes the note and sync (if they don't change it, it will never get resolved).
|
2019-07-29 15:43:53 +02:00
|
|
|
//
|
2017-07-24 20:58:11 +02:00
|
|
|
// This is compound with the fact that we can't have a reliable delta API on the file system so we need to check all the timestamps
|
|
|
|
// every time and rely on this exclusively to know about changes.
|
|
|
|
//
|
|
|
|
// This explains occasional failures of the fuzzing program (it finds that the clients end up with two different notes after sync). To
|
|
|
|
// check that it is indeed the problem, check log-database.txt of both clients, search for the note ID, and most likely both notes
|
|
|
|
// will have been modified at the same exact second at some point. If not, it's another bug that needs to be investigated.
|
2017-07-16 14:53:59 +02:00
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
export default class FileApiDriverLocal {
|
|
|
|
public static fsDriver_: FsDriverBase;
|
|
|
|
|
|
|
|
private fsErrorToJsError_(error: JoplinError, path: string|null = null) {
|
2017-07-03 20:58:01 +02:00
|
|
|
let msg = error.toString();
|
2019-09-19 23:51:18 +02:00
|
|
|
if (path !== null) msg += `. Path: ${path}`;
|
2024-04-25 14:31:48 +02:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Partial refactor of old coe from before rule was applied
|
|
|
|
const output: any = new Error(msg);
|
2017-07-03 20:58:01 +02:00
|
|
|
if (error.code) output.code = error.code;
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public fsDriver() {
|
2022-07-10 16:26:24 +02:00
|
|
|
if (!FileApiDriverLocal.fsDriver_) { throw new Error('FileApiDriverLocal.fsDriver_ not set!'); }
|
2018-01-17 20:51:15 +02:00
|
|
|
return FileApiDriverLocal.fsDriver_;
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async stat(path: string) {
|
2018-01-17 20:51:15 +02:00
|
|
|
try {
|
|
|
|
const s = await this.fsDriver().stat(path);
|
2018-01-17 23:01:41 +02:00
|
|
|
if (!s) return null;
|
2018-01-17 20:51:15 +02:00
|
|
|
return this.metadataFromStat_(s);
|
|
|
|
} catch (error) {
|
|
|
|
throw this.fsErrorToJsError_(error);
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
private metadataFromStat_(stat: Stat): ItemStat {
|
2017-06-11 23:11:14 +02:00
|
|
|
return {
|
2018-01-17 20:51:15 +02:00
|
|
|
path: stat.path,
|
2018-02-07 22:42:52 +02:00
|
|
|
// created_time: stat.birthtime.getTime(),
|
2018-01-17 20:51:15 +02:00
|
|
|
updated_time: stat.mtime.getTime(),
|
|
|
|
isDir: stat.isDirectory(),
|
2017-06-11 23:11:14 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
private metadataFromStats_(stats: Stat[]): ItemStat[] {
|
2020-03-14 01:46:14 +02:00
|
|
|
const output = [];
|
2018-01-17 20:51:15 +02:00
|
|
|
for (let i = 0; i < stats.length; i++) {
|
|
|
|
const mdStat = this.metadataFromStat_(stats[i]);
|
|
|
|
output.push(mdStat);
|
|
|
|
}
|
|
|
|
return output;
|
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async setTimestamp(path: string, timestampMs: number) {
|
2018-01-17 20:51:15 +02:00
|
|
|
try {
|
|
|
|
await this.fsDriver().setTimestamp(path, new Date(timestampMs));
|
|
|
|
} catch (error) {
|
|
|
|
throw this.fsErrorToJsError_(error);
|
|
|
|
}
|
2017-06-12 23:56:27 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async delta(path: string, options: DeltaOptions) {
|
|
|
|
const getStatFn = async (path: string) => {
|
2018-01-21 21:45:32 +02:00
|
|
|
const stats = await this.fsDriver().readDirStats(path);
|
|
|
|
return this.metadataFromStats_(stats);
|
|
|
|
};
|
2017-10-26 23:56:32 +02:00
|
|
|
|
2017-07-19 00:14:20 +02:00
|
|
|
try {
|
2018-01-21 21:45:32 +02:00
|
|
|
const output = await basicDelta(path, getStatFn, options);
|
|
|
|
return output;
|
2019-07-29 15:43:53 +02:00
|
|
|
} catch (error) {
|
2018-01-17 23:01:41 +02:00
|
|
|
throw this.fsErrorToJsError_(error, path);
|
2017-07-19 00:14:20 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async list(path: string) {
|
2017-07-03 20:58:01 +02:00
|
|
|
try {
|
2018-01-17 20:51:15 +02:00
|
|
|
const stats = await this.fsDriver().readDirStats(path);
|
|
|
|
const output = this.metadataFromStats_(stats);
|
2017-06-11 23:11:14 +02:00
|
|
|
|
2017-07-03 20:58:01 +02:00
|
|
|
return {
|
|
|
|
items: output,
|
|
|
|
hasMore: false,
|
2024-04-25 14:31:48 +02:00
|
|
|
context: null as unknown,
|
2017-07-03 20:58:01 +02:00
|
|
|
};
|
2019-07-29 15:43:53 +02:00
|
|
|
} catch (error) {
|
2018-01-17 23:01:41 +02:00
|
|
|
throw this.fsErrorToJsError_(error, path);
|
2017-07-03 20:58:01 +02:00
|
|
|
}
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async get(path: string, options: GetOptions) {
|
2022-07-10 16:26:24 +02:00
|
|
|
if (!options) options = {};
|
2017-07-02 14:02:07 +02:00
|
|
|
let output = null;
|
|
|
|
|
|
|
|
try {
|
2017-12-14 23:12:02 +02:00
|
|
|
if (options.target === 'file') {
|
2019-10-09 21:35:13 +02:00
|
|
|
// output = await fs.copy(path, options.path, { overwrite: true });
|
2018-01-17 20:51:15 +02:00
|
|
|
output = await this.fsDriver().copy(path, options.path);
|
2017-07-02 14:02:07 +02:00
|
|
|
} else {
|
2019-10-09 21:35:13 +02:00
|
|
|
// output = await fs.readFile(path, options.encoding);
|
2018-01-17 20:51:15 +02:00
|
|
|
output = await this.fsDriver().readFile(path, options.encoding);
|
2017-07-02 14:02:07 +02:00
|
|
|
}
|
|
|
|
} catch (error) {
|
2022-07-23 11:33:12 +02:00
|
|
|
if (error.code === 'ENOENT') return null;
|
2018-01-17 23:01:41 +02:00
|
|
|
throw this.fsErrorToJsError_(error, path);
|
2017-07-02 14:02:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return output;
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async mkdir(path: string) {
|
2018-01-17 20:51:15 +02:00
|
|
|
if (await this.fsDriver().exists(path)) return;
|
|
|
|
|
|
|
|
try {
|
|
|
|
await this.fsDriver().mkdir(path);
|
|
|
|
} catch (error) {
|
2018-01-17 23:01:41 +02:00
|
|
|
throw this.fsErrorToJsError_(error, path);
|
2018-01-17 20:51:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// return new Promise((resolve, reject) => {
|
|
|
|
// fs.exists(path, (exists) => {
|
|
|
|
// if (exists) {
|
|
|
|
// resolve();
|
|
|
|
// return;
|
|
|
|
// }
|
2019-07-29 15:43:53 +02:00
|
|
|
|
2018-01-17 20:51:15 +02:00
|
|
|
// fs.mkdirp(path, (error) => {
|
|
|
|
// if (error) {
|
|
|
|
// reject(this.fsErrorToJsError_(error));
|
|
|
|
// } else {
|
|
|
|
// resolve();
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// });
|
|
|
|
// });
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async put(path: string, content: string, options: PutOptions = null) {
|
2017-12-18 22:47:25 +02:00
|
|
|
if (!options) options = {};
|
|
|
|
|
2018-01-17 20:51:15 +02:00
|
|
|
try {
|
|
|
|
if (options.source === 'file') {
|
|
|
|
await this.fsDriver().copy(options.path, path);
|
|
|
|
return;
|
|
|
|
}
|
2019-07-29 15:43:53 +02:00
|
|
|
|
2018-01-17 20:51:15 +02:00
|
|
|
await this.fsDriver().writeFile(path, content, 'utf8');
|
|
|
|
} catch (error) {
|
2018-01-17 23:01:41 +02:00
|
|
|
throw this.fsErrorToJsError_(error, path);
|
2018-01-17 20:51:15 +02:00
|
|
|
}
|
|
|
|
// if (!options) options = {};
|
|
|
|
|
|
|
|
// if (options.source === 'file') content = await fs.readFile(options.path);
|
|
|
|
|
|
|
|
// return new Promise((resolve, reject) => {
|
|
|
|
// fs.writeFile(path, content, function(error) {
|
|
|
|
// if (error) {
|
|
|
|
// reject(this.fsErrorToJsError_(error));
|
|
|
|
// } else {
|
|
|
|
// resolve();
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// });
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async delete(path: string) {
|
2018-01-17 20:51:15 +02:00
|
|
|
try {
|
|
|
|
await this.fsDriver().unlink(path);
|
|
|
|
} catch (error) {
|
2018-01-17 23:01:41 +02:00
|
|
|
throw this.fsErrorToJsError_(error, path);
|
2018-01-17 20:51:15 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// return new Promise((resolve, reject) => {
|
|
|
|
// fs.unlink(path, function(error) {
|
|
|
|
// if (error) {
|
|
|
|
// if (error && error.code == 'ENOENT') {
|
|
|
|
// // File doesn't exist - it's fine
|
|
|
|
// resolve();
|
|
|
|
// } else {
|
|
|
|
// reject(this.fsErrorToJsError_(error));
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// resolve();
|
|
|
|
// }
|
|
|
|
// });
|
|
|
|
// });
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async move(oldPath: string, newPath: string) {
|
2018-01-17 23:01:41 +02:00
|
|
|
try {
|
|
|
|
await this.fsDriver().move(oldPath, newPath);
|
|
|
|
} catch (error) {
|
2019-07-30 09:35:42 +02:00
|
|
|
throw this.fsErrorToJsError_(error, oldPath);
|
2018-01-17 23:01:41 +02:00
|
|
|
}
|
2018-01-17 20:51:15 +02:00
|
|
|
|
|
|
|
// let lastError = null;
|
2019-07-29 15:43:53 +02:00
|
|
|
|
2018-01-17 20:51:15 +02:00
|
|
|
// for (let i = 0; i < 5; i++) {
|
|
|
|
// try {
|
|
|
|
// let output = await fs.move(oldPath, newPath, { overwrite: true });
|
|
|
|
// return output;
|
|
|
|
// } catch (error) {
|
|
|
|
// lastError = error;
|
|
|
|
// // Normally cannot happen with the `overwrite` flag but sometime it still does.
|
|
|
|
// // In this case, retry.
|
|
|
|
// if (error.code == 'EEXIST') {
|
|
|
|
// await time.sleep(1);
|
|
|
|
// continue;
|
|
|
|
// }
|
|
|
|
// throw this.fsErrorToJsError_(error);
|
|
|
|
// }
|
|
|
|
// }
|
2017-07-02 12:34:07 +02:00
|
|
|
|
2018-01-17 20:51:15 +02:00
|
|
|
// throw lastError;
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public format() {
|
2017-06-13 22:58:17 +02:00
|
|
|
throw new Error('Not supported');
|
|
|
|
}
|
|
|
|
|
2024-04-25 14:31:48 +02:00
|
|
|
public async clearRoot(baseDir: string) {
|
2022-07-10 16:26:24 +02:00
|
|
|
if (baseDir.startsWith('content://')) {
|
|
|
|
const result = await this.list(baseDir);
|
|
|
|
for (const item of result.items) {
|
|
|
|
await this.fsDriver().remove(item.path);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
await this.fsDriver().remove(baseDir);
|
|
|
|
await this.fsDriver().mkdir(baseDir);
|
|
|
|
}
|
2018-01-25 23:15:58 +02:00
|
|
|
}
|
2017-06-11 23:11:14 +02:00
|
|
|
}
|