diff --git a/.eslintignore b/.eslintignore index acb6112e28..75b4314a20 100644 --- a/.eslintignore +++ b/.eslintignore @@ -777,6 +777,7 @@ packages/lib/ObjectUtils.js packages/lib/PoorManIntervals.js packages/lib/RotatingLogs.test.js packages/lib/RotatingLogs.js +packages/lib/SyncTargetFilesystem.js packages/lib/SyncTargetJoplinCloud.js packages/lib/SyncTargetJoplinServer.js packages/lib/SyncTargetNone.js @@ -816,6 +817,7 @@ packages/lib/errorUtils.js packages/lib/errors.js packages/lib/eventManager.js packages/lib/file-api-driver-joplinServer.js +packages/lib/file-api-driver-local.js packages/lib/file-api-driver-memory.js packages/lib/file-api-driver.test.js packages/lib/file-api.test.js diff --git a/.gitignore b/.gitignore index f3740fe61d..dbc43d626b 100644 --- a/.gitignore +++ b/.gitignore @@ -757,6 +757,7 @@ packages/lib/ObjectUtils.js packages/lib/PoorManIntervals.js packages/lib/RotatingLogs.test.js packages/lib/RotatingLogs.js +packages/lib/SyncTargetFilesystem.js packages/lib/SyncTargetJoplinCloud.js packages/lib/SyncTargetJoplinServer.js packages/lib/SyncTargetNone.js @@ -796,6 +797,7 @@ packages/lib/errorUtils.js packages/lib/errors.js packages/lib/eventManager.js packages/lib/file-api-driver-joplinServer.js +packages/lib/file-api-driver-local.js packages/lib/file-api-driver-memory.js packages/lib/file-api-driver.test.js packages/lib/file-api.test.js diff --git a/packages/app-cli/app/main.js b/packages/app-cli/app/main.js index 28530476bd..cd76662ff9 100644 --- a/packages/app-cli/app/main.js +++ b/packages/app-cli/app/main.js @@ -26,7 +26,7 @@ const sharp = require('sharp'); const { shimInit } = require('@joplin/lib/shim-init-node.js'); const shim = require('@joplin/lib/shim').default; const { _ } = require('@joplin/lib/locale'); -const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local'); +const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default; const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default; const envFromArgs = require('@joplin/lib/envFromArgs'); const nodeSqlite = require('sqlite3'); diff --git a/packages/app-desktop/main-html.js b/packages/app-desktop/main-html.js index 2da53e78d9..734d4600e3 100644 --- a/packages/app-desktop/main-html.js +++ b/packages/app-desktop/main-html.js @@ -26,7 +26,7 @@ const shim = require('@joplin/lib/shim').default; const { shimInit } = require('@joplin/lib/shim-init-node.js'); const bridge = require('@electron/remote').require('./bridge').default; const EncryptionService = require('@joplin/lib/services/e2ee/EncryptionService').default; -const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local'); +const FileApiDriverLocal = require('@joplin/lib/file-api-driver-local').default; const React = require('react'); const nodeSqlite = require('sqlite3'); const initLib = require('@joplin/lib/initLib').default; diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 41ca55a9f6..e1cd8f61e0 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -72,13 +72,13 @@ const { SideMenuContentNote } = require('./components/side-menu-content-note.js' const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native'); import { reg } from '@joplin/lib/registry'; const { defaultState } = require('@joplin/lib/reducer'); -const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local'); +import FileApiDriverLocal from '@joplin/lib/file-api-driver-local'; import ResourceFetcher from '@joplin/lib/services/ResourceFetcher'; import SearchEngine from '@joplin/lib/services/search/SearchEngine'; import WelcomeUtils from '@joplin/lib/WelcomeUtils'; import { themeStyle } from './components/global-style'; import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry'; -const SyncTargetFilesystem = require('@joplin/lib/SyncTargetFilesystem.js'); +import SyncTargetFilesystem from '@joplin/lib/SyncTargetFilesystem'; const SyncTargetNextcloud = require('@joplin/lib/SyncTargetNextcloud.js'); const SyncTargetWebDAV = require('@joplin/lib/SyncTargetWebDAV.js'); const SyncTargetDropbox = require('@joplin/lib/SyncTargetDropbox.js'); diff --git a/packages/lib/BaseApplication.ts b/packages/lib/BaseApplication.ts index 8fb0515a4c..f5cd69f76a 100644 --- a/packages/lib/BaseApplication.ts +++ b/packages/lib/BaseApplication.ts @@ -30,7 +30,7 @@ import fs = require('fs-extra'); const EventEmitter = require('events'); const syswidecas = require('./vendor/syswide-cas'); import SyncTargetRegistry from './SyncTargetRegistry'; -const SyncTargetFilesystem = require('./SyncTargetFilesystem.js'); +import SyncTargetFilesystem from './SyncTargetFilesystem'; const SyncTargetNextcloud = require('./SyncTargetNextcloud.js'); const SyncTargetWebDAV = require('./SyncTargetWebDAV.js'); const SyncTargetDropbox = require('./SyncTargetDropbox.js'); diff --git a/packages/lib/SyncTargetFilesystem.js b/packages/lib/SyncTargetFilesystem.js deleted file mode 100644 index dce02a781a..0000000000 --- a/packages/lib/SyncTargetFilesystem.js +++ /dev/null @@ -1,44 +0,0 @@ -const BaseSyncTarget = require('./BaseSyncTarget').default; -const { _ } = require('./locale'); -const Setting = require('./models/Setting').default; -const { FileApi } = require('./file-api.js'); -const { FileApiDriverLocal } = require('./file-api-driver-local'); -const Synchronizer = require('./Synchronizer').default; - -class SyncTargetFilesystem extends BaseSyncTarget { - static id() { - return 2; - } - - static targetName() { - return 'filesystem'; - } - - static label() { - return _('File system'); - } - - static unsupportedPlatforms() { - return ['ios']; - } - - async isAuthenticated() { - return true; - } - - async initFileApi() { - const syncPath = Setting.value('sync.2.path'); - const driver = new FileApiDriverLocal(); - const fileApi = new FileApi(syncPath, driver); - fileApi.setLogger(this.logger()); - fileApi.setSyncTargetId(SyncTargetFilesystem.id()); - await driver.mkdir(syncPath); - return fileApi; - } - - async initSynchronizer() { - return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType')); - } -} - -module.exports = SyncTargetFilesystem; diff --git a/packages/lib/SyncTargetFilesystem.ts b/packages/lib/SyncTargetFilesystem.ts new file mode 100644 index 0000000000..3116c87535 --- /dev/null +++ b/packages/lib/SyncTargetFilesystem.ts @@ -0,0 +1,43 @@ +import BaseSyncTarget from './BaseSyncTarget'; +import { _ } from './locale'; +import Setting from './models/Setting'; +import { FileApi } from './file-api'; +import FileApiDriverLocal from './file-api-driver-local'; +import Synchronizer from './Synchronizer'; + +export default class SyncTargetFilesystem extends BaseSyncTarget { + public static id() { + return 2; + } + + public static targetName() { + return 'filesystem'; + } + + public static label() { + return _('File system'); + } + + public static unsupportedPlatforms() { + return ['ios']; + } + + public async isAuthenticated() { + return true; + } + + public async initFileApi() { + const syncPath = Setting.value('sync.2.path'); + const driver = new FileApiDriverLocal(); + const fileApi = new FileApi(syncPath, driver); + fileApi.setLogger(this.logger()); + fileApi.setSyncTargetId(SyncTargetFilesystem.id()); + await driver.mkdir(syncPath); + return fileApi; + } + + public async initSynchronizer() { + return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType')); + } +} + diff --git a/packages/lib/file-api-driver-local.js b/packages/lib/file-api-driver-local.ts similarity index 82% rename from packages/lib/file-api-driver-local.js rename to packages/lib/file-api-driver-local.ts index 7f042dd38c..ff2b07d6cd 100644 --- a/packages/lib/file-api-driver-local.js +++ b/packages/lib/file-api-driver-local.ts @@ -1,4 +1,7 @@ -const { basicDelta } = require('./file-api'); +import JoplinError from './JoplinError'; + +import { DeltaOptions, GetOptions, ItemStat, PutOptions, basicDelta } from './file-api'; +import FsDriverBase, { Stat } from './fs-driver-base'; // 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, @@ -13,21 +16,24 @@ const { basicDelta } = require('./file-api'); // 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) { +export default class FileApiDriverLocal { + public static fsDriver_: FsDriverBase; + + private fsErrorToJsError_(error: JoplinError, path: string|null = null) { let msg = error.toString(); if (path !== null) msg += `. Path: ${path}`; - const output = new Error(msg); + // 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); if (error.code) output.code = error.code; return output; } - fsDriver() { + public fsDriver() { if (!FileApiDriverLocal.fsDriver_) { throw new Error('FileApiDriverLocal.fsDriver_ not set!'); } return FileApiDriverLocal.fsDriver_; } - async stat(path) { + public async stat(path: string) { try { const s = await this.fsDriver().stat(path); if (!s) return null; @@ -37,7 +43,7 @@ class FileApiDriverLocal { } } - metadataFromStat_(stat) { + private metadataFromStat_(stat: Stat): ItemStat { return { path: stat.path, // created_time: stat.birthtime.getTime(), @@ -46,7 +52,7 @@ class FileApiDriverLocal { }; } - metadataFromStats_(stats) { + private metadataFromStats_(stats: Stat[]): ItemStat[] { const output = []; for (let i = 0; i < stats.length; i++) { const mdStat = this.metadataFromStat_(stats[i]); @@ -55,7 +61,7 @@ class FileApiDriverLocal { return output; } - async setTimestamp(path, timestampMs) { + public async setTimestamp(path: string, timestampMs: number) { try { await this.fsDriver().setTimestamp(path, new Date(timestampMs)); } catch (error) { @@ -63,8 +69,8 @@ class FileApiDriverLocal { } } - async delta(path, options) { - const getStatFn = async path => { + public async delta(path: string, options: DeltaOptions) { + const getStatFn = async (path: string) => { const stats = await this.fsDriver().readDirStats(path); return this.metadataFromStats_(stats); }; @@ -77,7 +83,7 @@ class FileApiDriverLocal { } } - async list(path) { + public async list(path: string) { try { const stats = await this.fsDriver().readDirStats(path); const output = this.metadataFromStats_(stats); @@ -85,14 +91,14 @@ class FileApiDriverLocal { return { items: output, hasMore: false, - context: null, + context: null as unknown, }; } catch (error) { throw this.fsErrorToJsError_(error, path); } } - async get(path, options) { + public async get(path: string, options: GetOptions) { if (!options) options = {}; let output = null; @@ -112,7 +118,7 @@ class FileApiDriverLocal { return output; } - async mkdir(path) { + public async mkdir(path: string) { if (await this.fsDriver().exists(path)) return; try { @@ -139,7 +145,7 @@ class FileApiDriverLocal { // }); } - async put(path, content, options = null) { + public async put(path: string, content: string, options: PutOptions = null) { if (!options) options = {}; try { @@ -167,7 +173,7 @@ class FileApiDriverLocal { // }); } - async delete(path) { + public async delete(path: string) { try { await this.fsDriver().unlink(path); } catch (error) { @@ -190,7 +196,7 @@ class FileApiDriverLocal { // }); } - async move(oldPath, newPath) { + public async move(oldPath: string, newPath: string) { try { await this.fsDriver().move(oldPath, newPath); } catch (error) { @@ -218,11 +224,11 @@ class FileApiDriverLocal { // throw lastError; } - format() { + public format() { throw new Error('Not supported'); } - async clearRoot(baseDir) { + public async clearRoot(baseDir: string) { if (baseDir.startsWith('content://')) { const result = await this.list(baseDir); for (const item of result.items) { @@ -234,5 +240,3 @@ class FileApiDriverLocal { } } } - -module.exports = { FileApiDriverLocal }; diff --git a/packages/lib/file-api.ts b/packages/lib/file-api.ts index 7c70035960..b35ffe5aa5 100644 --- a/packages/lib/file-api.ts +++ b/packages/lib/file-api.ts @@ -1,4 +1,4 @@ -import Logger from '@joplin/utils/Logger'; +import Logger, { LoggerWrapper } from '@joplin/utils/Logger'; import shim from './shim'; import BaseItem from './models/BaseItem'; import time from './time'; @@ -100,6 +100,36 @@ async function tryAndRepeat(fn: Function, count: number) { } } +export interface DeltaOptions { + allItemIdsHandler(): Promise; + logger?: LoggerWrapper; + wipeOutFailSafe: boolean; +} + +export enum GetOptionsTarget { + String = 'string', + File = 'file', +} + +export interface GetOptions { + target?: GetOptionsTarget; + path?: string; + encoding?: string; + + source?: string; +} + +export interface PutOptions { + path?: string; + source?: string; +} + +export interface ItemStat { + path: string; + updated_time: number; + isDir: boolean; +} + class FileApi { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied @@ -316,7 +346,7 @@ class FileApi { return tryAndRepeat(() => this.driver_.mkdir(this.fullPath(path)), this.requestRepeatCount()); } - public async stat(path: string) { + public async stat(path: string): Promise { logger.debug(`stat ${this.fullPath(path)}`); const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath(path)), this.requestRepeatCount()); @@ -327,8 +357,7 @@ class FileApi { } // Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'` - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public get(path: string, options: any = null) { + public get(path: string, options: GetOptions = null) { if (!options) options = {}; if (!options.encoding) options.encoding = 'utf8'; logger.debug(`get ${this.fullPath(path)}`); @@ -336,7 +365,7 @@ class FileApi { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public async put(path: string, content: any, options: any = null) { + public async put(path: string, content: any, options: PutOptions = null) { logger.debug(`put ${this.fullPath(path)}`, options); if (options && options.source === 'file') { @@ -372,8 +401,7 @@ class FileApi { return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount()); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - public delta(path: string, options: any = null): Promise { + public delta(path: string, options: DeltaOptions|null = null): Promise { logger.debug(`delta ${this.fullPath(path)}`); return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount()); } @@ -423,7 +451,7 @@ function basicDeltaContextFromOptions_(options: any) { // a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously // the file system do not. // eslint-disable-next-line @typescript-eslint/ban-types, @typescript-eslint/no-explicit-any -- Old code before rule was applied, Old code before rule was applied -async function basicDelta(path: string, getDirStatFn: Function, options: any) { +async function basicDelta(path: string, getDirStatFn: Function, options: DeltaOptions) { const outputLimit = 50; const itemIds = await options.allItemIdsHandler(); if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided'); diff --git a/packages/lib/fs-driver-base.ts b/packages/lib/fs-driver-base.ts index 8d062cc610..f776bbb068 100644 --- a/packages/lib/fs-driver-base.ts +++ b/packages/lib/fs-driver-base.ts @@ -84,6 +84,10 @@ export default class FsDriverBase { throw new Error('Not implemented: remove'); } + public async setTimestamp(_path: string, _timestampDate: Date): Promise { + throw new Error('Not implemented: setTimestamp'); + } + public async isDirectory(path: string) { const stat = await this.stat(path); return !stat ? false : stat.isDirectory(); diff --git a/packages/lib/services/synchronizer/Synchronizer.resources.test.ts b/packages/lib/services/synchronizer/Synchronizer.resources.test.ts index 225ad5de05..aee08b037a 100644 --- a/packages/lib/services/synchronizer/Synchronizer.resources.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.resources.test.ts @@ -375,6 +375,7 @@ describe('Synchronizer.resources', () => { const resource: ResourceEntity = (await Resource.all())[0]; const resourcePath = `.resource/${resource.id}`; + await synchronizer().api().mkdir('.resource/'); await synchronizer().api().put(resourcePath, 'before upload'); expect(await synchronizer().api().get(resourcePath)).toBe('before upload'); await synchronizerStart(); diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index 431d3d3bb7..9f4afa842b 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -13,7 +13,7 @@ import { DownloadController } from './downloadController'; import { TextItem } from 'pdfjs-dist/types/src/display/api'; import replaceUnsupportedCharacters from './utils/replaceUnsupportedCharacters'; -const { FileApiDriverLocal } = require('./file-api-driver-local'); +import FileApiDriverLocal from './file-api-driver-local'; const mimeUtils = require('./mime-utils.js').mime; const { _ } = require('./locale'); const http = require('http'); diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index d0b5d43e9e..8de50090d4 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -1,6 +1,7 @@ import * as React from 'react'; import { NoteEntity, ResourceEntity } from './services/database/types'; import type FsDriverBase from './fs-driver-base'; +import type FileApiDriverLocal from './file-api-driver-local'; export interface CreateResourceFromPathOptions { resizeLargeImages?: 'always' | 'never' | 'ask'; @@ -260,8 +261,7 @@ const shim = { throw new Error('Not implemented: fsDriver'); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied - FileApiDriverLocal: null as any, + FileApiDriverLocal: null as typeof FileApiDriverLocal, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied readLocalFileBase64: (_path: string): any => { diff --git a/packages/lib/testing/test-utils.ts b/packages/lib/testing/test-utils.ts index 4a0d284a85..be95b19834 100644 --- a/packages/lib/testing/test-utils.ts +++ b/packages/lib/testing/test-utils.ts @@ -30,13 +30,13 @@ import MasterKey from '../models/MasterKey'; import BaseItem from '../models/BaseItem'; import { FileApi } from '../file-api'; const FileApiDriverMemory = require('../file-api-driver-memory').default; -const { FileApiDriverLocal } = require('../file-api-driver-local'); +import FileApiDriverLocal from '../file-api-driver-local'; const { FileApiDriverWebDav } = require('../file-api-driver-webdav.js'); const { FileApiDriverDropbox } = require('../file-api-driver-dropbox.js'); const { FileApiDriverOneDrive } = require('../file-api-driver-onedrive.js'); import SyncTargetRegistry from '../SyncTargetRegistry'; const SyncTargetMemory = require('../SyncTargetMemory.js'); -const SyncTargetFilesystem = require('../SyncTargetFilesystem.js'); +import SyncTargetFilesystem from '../SyncTargetFilesystem'; const SyncTargetNextcloud = require('../SyncTargetNextcloud.js'); const SyncTargetDropbox = require('../SyncTargetDropbox.js'); const SyncTargetAmazonS3 = require('../SyncTargetAmazonS3.js');