1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-24 08:12:24 +02:00
joplin/packages/lib/file-api-driver-local.js
2022-07-23 11:33:12 +02:00

239 lines
6.3 KiB
JavaScript

const { basicDelta } = require('./file-api');
// 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).
//
// 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.
class FileApiDriverLocal {
fsErrorToJsError_(error, path = null) {
let msg = error.toString();
if (path !== null) msg += `. Path: ${path}`;
const output = new Error(msg);
if (error.code) output.code = error.code;
return output;
}
fsDriver() {
if (!FileApiDriverLocal.fsDriver_) { throw new Error('FileApiDriverLocal.fsDriver_ not set!'); }
return FileApiDriverLocal.fsDriver_;
}
async stat(path) {
try {
const s = await this.fsDriver().stat(path);
if (!s) return null;
return this.metadataFromStat_(s);
} catch (error) {
throw this.fsErrorToJsError_(error);
}
}
metadataFromStat_(stat) {
return {
path: stat.path,
// created_time: stat.birthtime.getTime(),
updated_time: stat.mtime.getTime(),
isDir: stat.isDirectory(),
};
}
metadataFromStats_(stats) {
const output = [];
for (let i = 0; i < stats.length; i++) {
const mdStat = this.metadataFromStat_(stats[i]);
output.push(mdStat);
}
return output;
}
async setTimestamp(path, timestampMs) {
try {
await this.fsDriver().setTimestamp(path, new Date(timestampMs));
} catch (error) {
throw this.fsErrorToJsError_(error);
}
}
async delta(path, options) {
const getStatFn = async path => {
const stats = await this.fsDriver().readDirStats(path);
return this.metadataFromStats_(stats);
};
try {
const output = await basicDelta(path, getStatFn, options);
return output;
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
async list(path) {
try {
const stats = await this.fsDriver().readDirStats(path);
const output = this.metadataFromStats_(stats);
return {
items: output,
hasMore: false,
context: null,
};
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
}
async get(path, options) {
if (!options) options = {};
let output = null;
try {
if (options.target === 'file') {
// output = await fs.copy(path, options.path, { overwrite: true });
output = await this.fsDriver().copy(path, options.path);
} else {
// output = await fs.readFile(path, options.encoding);
output = await this.fsDriver().readFile(path, options.encoding);
}
} catch (error) {
if (error.code === 'ENOENT') return null;
throw this.fsErrorToJsError_(error, path);
}
return output;
}
async mkdir(path) {
if (await this.fsDriver().exists(path)) return;
try {
await this.fsDriver().mkdir(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
// return new Promise((resolve, reject) => {
// fs.exists(path, (exists) => {
// if (exists) {
// resolve();
// return;
// }
// fs.mkdirp(path, (error) => {
// if (error) {
// reject(this.fsErrorToJsError_(error));
// } else {
// resolve();
// }
// });
// });
// });
}
async put(path, content, options = null) {
if (!options) options = {};
try {
if (options.source === 'file') {
await this.fsDriver().copy(options.path, path);
return;
}
await this.fsDriver().writeFile(path, content, 'utf8');
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
// 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();
// }
// });
// });
}
async delete(path) {
try {
await this.fsDriver().unlink(path);
} catch (error) {
throw this.fsErrorToJsError_(error, path);
}
// 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();
// }
// });
// });
}
async move(oldPath, newPath) {
try {
await this.fsDriver().move(oldPath, newPath);
} catch (error) {
throw this.fsErrorToJsError_(error, oldPath);
}
// let lastError = null;
// 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);
// }
// }
// throw lastError;
}
format() {
throw new Error('Not supported');
}
async clearRoot(baseDir) {
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);
}
}
}
module.exports = { FileApiDriverLocal };