1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-02-01 07:49:31 +02:00

Compare commits

..

32 Commits

Author SHA1 Message Date
Laurent Cozic
a5071861b5 doc 2021-01-29 18:42:23 +00:00
Laurent Cozic
77080dc4dc style 2021-01-29 18:25:12 +00:00
Laurent Cozic
16c383092e add tests 2021-01-29 17:59:48 +00:00
Laurent Cozic
9a440053f5 fixed test 2021-01-29 17:30:23 +00:00
Laurent Cozic
10e560b652 shared status 2021-01-29 17:21:45 +00:00
Laurent Cozic
407a101906 test 2021-01-29 14:44:02 +00:00
Laurent Cozic
ac273619b6 add tests 2021-01-29 14:24:37 +00:00
Laurent Cozic
919c21da2a Disable support for note links 2021-01-28 23:10:58 +00:00
Laurent Cozic
1e865a4624 eslint 2021-01-28 23:00:55 +00:00
Laurent Cozic
37b644402c ren 2021-01-28 22:54:22 +00:00
Laurent Cozic
6ca1a56ce8 update 2021-01-28 22:51:40 +00:00
Laurent Cozic
c9406e350f note style 2021-01-28 12:55:08 +00:00
Laurent Cozic
c92447b7b9 Merge branch 'dev' into server_share_link 2021-01-28 10:22:53 +00:00
Laurent Cozic
42c80e6e28 Merge branch 'release-1.7' into dev 2021-01-28 10:22:33 +00:00
Caleb John
de5bc45300 Change the Desktop check in the linux update script (#4405) 2021-01-27 21:45:49 +00:00
Vaso3
f60f07ad19 All: Translation: Update sr_RS.po (#4429)
Corrected and added translation in the first 600 lines of the file.
2021-01-26 20:46:07 -05:00
Mats Schade
4e8299d444 Doc: Corrected WSL link in readme (#4423) 2021-01-26 20:42:02 -05:00
Laurent Cozic
3037e59044 Fixed test 2021-01-27 01:30:52 +00:00
Laurent Cozic
c440a9dfb6 update 2021-01-27 01:12:13 +00:00
Laurent Cozic
fc4c46b780 update 2021-01-27 01:10:10 +00:00
Laurent Cozic
8447e95588 update 2021-01-26 23:44:05 +00:00
Laurent Cozic
6932decc1b remove Setting dependency in theme.ts 2021-01-26 16:34:02 +00:00
Laurent Cozic
0b15c9c976 update 2021-01-26 16:19:35 +00:00
Laurent Cozic
b23f26c1c5 update 2021-01-26 16:19:08 +00:00
Laurent Cozic
eaaa36199a refactor 2021-01-26 12:29:24 +00:00
Laurent Cozic
84eea0066b update 2021-01-26 10:42:07 +00:00
Laurent Cozic
56418dd628 refactor 2021-01-26 10:31:45 +00:00
Laurent Cozic
8cfcebdc19 update 2021-01-26 01:26:30 +00:00
Laurent Cozic
10be556ac3 ref 2021-01-25 23:49:06 +00:00
Laurent Cozic
b28d50536e tests 2021-01-25 23:02:33 +00:00
Laurent Cozic
1337c895d1 generics 2021-01-25 22:47:51 +00:00
Laurent Cozic
a8942fb876 update 2021-01-25 18:53:48 +00:00
149 changed files with 3467 additions and 5181 deletions

View File

@@ -97,15 +97,15 @@ packages/app-cli/tests/Synchronizer.resources.js.map
packages/app-cli/tests/Synchronizer.revisions.d.ts
packages/app-cli/tests/Synchronizer.revisions.js
packages/app-cli/tests/Synchronizer.revisions.js.map
packages/app-cli/tests/Synchronizer.sharing.d.ts
packages/app-cli/tests/Synchronizer.sharing.js
packages/app-cli/tests/Synchronizer.sharing.js.map
packages/app-cli/tests/Synchronizer.tags.d.ts
packages/app-cli/tests/Synchronizer.tags.js
packages/app-cli/tests/Synchronizer.tags.js.map
packages/app-cli/tests/fsDriver.d.ts
packages/app-cli/tests/fsDriver.js
packages/app-cli/tests/fsDriver.js.map
packages/app-cli/tests/htmlUtils.d.ts
packages/app-cli/tests/htmlUtils.js
packages/app-cli/tests/htmlUtils.js.map
packages/app-cli/tests/models_Folder.d.ts
packages/app-cli/tests/models_Folder.js
packages/app-cli/tests/models_Folder.js.map
@@ -787,15 +787,18 @@ packages/lib/BaseApplication.js.map
packages/lib/BaseModel.d.ts
packages/lib/BaseModel.js
packages/lib/BaseModel.js.map
packages/lib/BaseSyncTarget.d.ts
packages/lib/BaseSyncTarget.js
packages/lib/BaseSyncTarget.js.map
packages/lib/InMemoryCache.d.ts
packages/lib/InMemoryCache.js
packages/lib/InMemoryCache.js.map
packages/lib/JoplinDatabase.d.ts
packages/lib/JoplinDatabase.js
packages/lib/JoplinDatabase.js.map
packages/lib/JoplinServerApi.d.ts
packages/lib/JoplinServerApi.js
packages/lib/JoplinServerApi.js.map
packages/lib/JoplinServerApi2.d.ts
packages/lib/JoplinServerApi2.js
packages/lib/JoplinServerApi2.js.map
packages/lib/Logger.d.ts
packages/lib/Logger.js
packages/lib/Logger.js.map
@@ -820,6 +823,9 @@ packages/lib/commands/historyForward.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map
packages/lib/dummy.test.d.ts
packages/lib/dummy.test.js
packages/lib/dummy.test.js.map
@@ -832,15 +838,15 @@ packages/lib/eventManager.js.map
packages/lib/file-api-driver-joplinServer.d.ts
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-joplinServer.js.map
packages/lib/file-api.d.ts
packages/lib/file-api.js
packages/lib/file-api.js.map
packages/lib/fs-driver-base.d.ts
packages/lib/fs-driver-base.js
packages/lib/fs-driver-base.js.map
packages/lib/fs-driver-node.d.ts
packages/lib/fs-driver-node.js
packages/lib/fs-driver-node.js.map
packages/lib/htmlUtils.d.ts
packages/lib/htmlUtils.js
packages/lib/htmlUtils.js.map
packages/lib/import-enex-md-gen.d.ts
packages/lib/import-enex-md-gen.js
packages/lib/import-enex-md-gen.js.map
@@ -922,6 +928,9 @@ packages/lib/path-utils.js.map
packages/lib/reducer.d.ts
packages/lib/reducer.js
packages/lib/reducer.js.map
packages/lib/registry.d.ts
packages/lib/registry.js
packages/lib/registry.js.map
packages/lib/services/AlarmService.d.ts
packages/lib/services/AlarmService.js
packages/lib/services/AlarmService.js.map
@@ -1438,219 +1447,9 @@ packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
packages/server/src/app.d.ts
packages/server/src/app.js
packages/server/src/app.js.map
packages/server/src/config.d.ts
packages/server/src/config.js
packages/server/src/config.js.map
packages/server/src/db.d.ts
packages/server/src/db.js
packages/server/src/db.js.map
packages/server/src/middleware/notificationHandler.d.ts
packages/server/src/middleware/notificationHandler.js
packages/server/src/middleware/notificationHandler.js.map
packages/server/src/middleware/notificationHandler.test.d.ts
packages/server/src/middleware/notificationHandler.test.js
packages/server/src/middleware/notificationHandler.test.js.map
packages/server/src/middleware/ownerHandler.d.ts
packages/server/src/middleware/ownerHandler.js
packages/server/src/middleware/ownerHandler.js.map
packages/server/src/middleware/ownerHandler.test.d.ts
packages/server/src/middleware/ownerHandler.test.js
packages/server/src/middleware/ownerHandler.test.js.map
packages/server/src/middleware/routeHandler.d.ts
packages/server/src/middleware/routeHandler.js
packages/server/src/middleware/routeHandler.js.map
packages/server/src/migrations/20190913171451_create.d.ts
packages/server/src/migrations/20190913171451_create.js
packages/server/src/migrations/20190913171451_create.js.map
packages/server/src/migrations/20203012152842_notifications.d.ts
packages/server/src/migrations/20203012152842_notifications.js
packages/server/src/migrations/20203012152842_notifications.js.map
packages/server/src/models/ApiClientModel.d.ts
packages/server/src/models/ApiClientModel.js
packages/server/src/models/ApiClientModel.js.map
packages/server/src/models/BaseModel.d.ts
packages/server/src/models/BaseModel.js
packages/server/src/models/BaseModel.js.map
packages/server/src/models/ChangeModel.d.ts
packages/server/src/models/ChangeModel.js
packages/server/src/models/ChangeModel.js.map
packages/server/src/models/ChangeModel.test.d.ts
packages/server/src/models/ChangeModel.test.js
packages/server/src/models/ChangeModel.test.js.map
packages/server/src/models/FileModel.d.ts
packages/server/src/models/FileModel.js
packages/server/src/models/FileModel.js.map
packages/server/src/models/FileModel.test.d.ts
packages/server/src/models/FileModel.test.js
packages/server/src/models/FileModel.test.js.map
packages/server/src/models/NotificationModel.d.ts
packages/server/src/models/NotificationModel.js
packages/server/src/models/NotificationModel.js.map
packages/server/src/models/NotificationModel.test.d.ts
packages/server/src/models/NotificationModel.test.js
packages/server/src/models/NotificationModel.test.js.map
packages/server/src/models/PermissionModel.d.ts
packages/server/src/models/PermissionModel.js
packages/server/src/models/PermissionModel.js.map
packages/server/src/models/SessionModel.d.ts
packages/server/src/models/SessionModel.js
packages/server/src/models/SessionModel.js.map
packages/server/src/models/UserModel.d.ts
packages/server/src/models/UserModel.js
packages/server/src/models/UserModel.js.map
packages/server/src/models/UserModel.test.d.ts
packages/server/src/models/UserModel.test.js
packages/server/src/models/UserModel.test.js.map
packages/server/src/models/factory.d.ts
packages/server/src/models/factory.js
packages/server/src/models/factory.js.map
packages/server/src/models/utils/pagination.d.ts
packages/server/src/models/utils/pagination.js
packages/server/src/models/utils/pagination.js.map
packages/server/src/models/utils/pagination.test.d.ts
packages/server/src/models/utils/pagination.test.js
packages/server/src/models/utils/pagination.test.js.map
packages/server/src/routes/api/files.d.ts
packages/server/src/routes/api/files.js
packages/server/src/routes/api/files.js.map
packages/server/src/routes/api/files.test.d.ts
packages/server/src/routes/api/files.test.js
packages/server/src/routes/api/files.test.js.map
packages/server/src/routes/api/ping.d.ts
packages/server/src/routes/api/ping.js
packages/server/src/routes/api/ping.js.map
packages/server/src/routes/api/ping.test.d.ts
packages/server/src/routes/api/ping.test.js
packages/server/src/routes/api/ping.test.js.map
packages/server/src/routes/api/sessions.d.ts
packages/server/src/routes/api/sessions.js
packages/server/src/routes/api/sessions.js.map
packages/server/src/routes/api/sessions.test.d.ts
packages/server/src/routes/api/sessions.test.js
packages/server/src/routes/api/sessions.test.js.map
packages/server/src/routes/default.d.ts
packages/server/src/routes/default.js
packages/server/src/routes/default.js.map
packages/server/src/routes/index/files.d.ts
packages/server/src/routes/index/files.js
packages/server/src/routes/index/files.js.map
packages/server/src/routes/index/home.d.ts
packages/server/src/routes/index/home.js
packages/server/src/routes/index/home.js.map
packages/server/src/routes/index/home.test.d.ts
packages/server/src/routes/index/home.test.js
packages/server/src/routes/index/home.test.js.map
packages/server/src/routes/index/login.d.ts
packages/server/src/routes/index/login.js
packages/server/src/routes/index/login.js.map
packages/server/src/routes/index/login.test.d.ts
packages/server/src/routes/index/login.test.js
packages/server/src/routes/index/login.test.js.map
packages/server/src/routes/index/logout.d.ts
packages/server/src/routes/index/logout.js
packages/server/src/routes/index/logout.js.map
packages/server/src/routes/index/logout.test.d.ts
packages/server/src/routes/index/logout.test.js
packages/server/src/routes/index/logout.test.js.map
packages/server/src/routes/index/notifications.d.ts
packages/server/src/routes/index/notifications.js
packages/server/src/routes/index/notifications.js.map
packages/server/src/routes/index/notifications.test.d.ts
packages/server/src/routes/index/notifications.test.js
packages/server/src/routes/index/notifications.test.js.map
packages/server/src/routes/index/users.d.ts
packages/server/src/routes/index/users.js
packages/server/src/routes/index/users.js.map
packages/server/src/routes/index/users.test.d.ts
packages/server/src/routes/index/users.test.js
packages/server/src/routes/index/users.test.js.map
packages/server/src/routes/oauth2/authorize.d.ts
packages/server/src/routes/oauth2/authorize.js
packages/server/src/routes/oauth2/authorize.js.map
packages/server/src/routes/routes.d.ts
packages/server/src/routes/routes.js
packages/server/src/routes/routes.js.map
packages/server/src/services/MustacheService.d.ts
packages/server/src/services/MustacheService.js
packages/server/src/services/MustacheService.js.map
packages/server/src/tools/db-migrate.d.ts
packages/server/src/tools/db-migrate.js
packages/server/src/tools/db-migrate.js.map
packages/server/src/tools/dbTools.d.ts
packages/server/src/tools/dbTools.js
packages/server/src/tools/dbTools.js.map
packages/server/src/tools/generate-types.d.ts
packages/server/src/tools/generate-types.js
packages/server/src/tools/generate-types.js.map
packages/server/src/utils/Router.d.ts
packages/server/src/utils/Router.js
packages/server/src/utils/Router.js.map
packages/server/src/utils/TransactionHandler.d.ts
packages/server/src/utils/TransactionHandler.js
packages/server/src/utils/TransactionHandler.js.map
packages/server/src/utils/auth.d.ts
packages/server/src/utils/auth.js
packages/server/src/utils/auth.js.map
packages/server/src/utils/base64.d.ts
packages/server/src/utils/base64.js
packages/server/src/utils/base64.js.map
packages/server/src/utils/cache.d.ts
packages/server/src/utils/cache.js
packages/server/src/utils/cache.js.map
packages/server/src/utils/defaultView.d.ts
packages/server/src/utils/defaultView.js
packages/server/src/utils/defaultView.js.map
packages/server/src/utils/errors.d.ts
packages/server/src/utils/errors.js
packages/server/src/utils/errors.js.map
packages/server/src/utils/htmlUtils.d.ts
packages/server/src/utils/htmlUtils.js
packages/server/src/utils/htmlUtils.js.map
packages/server/src/utils/koaIf.d.ts
packages/server/src/utils/koaIf.js
packages/server/src/utils/koaIf.js.map
packages/server/src/utils/requestUtils.d.ts
packages/server/src/utils/requestUtils.js
packages/server/src/utils/requestUtils.js.map
packages/server/src/utils/routeUtils.d.ts
packages/server/src/utils/routeUtils.js
packages/server/src/utils/routeUtils.js.map
packages/server/src/utils/routeUtils.test.d.ts
packages/server/src/utils/routeUtils.test.js
packages/server/src/utils/routeUtils.test.js.map
packages/server/src/utils/testing/apiUtils.d.ts
packages/server/src/utils/testing/apiUtils.js
packages/server/src/utils/testing/apiUtils.js.map
packages/server/src/utils/testing/koa/FakeCookies.d.ts
packages/server/src/utils/testing/koa/FakeCookies.js
packages/server/src/utils/testing/koa/FakeCookies.js.map
packages/server/src/utils/testing/koa/FakeRequest.d.ts
packages/server/src/utils/testing/koa/FakeRequest.js
packages/server/src/utils/testing/koa/FakeRequest.js.map
packages/server/src/utils/testing/koa/FakeResponse.d.ts
packages/server/src/utils/testing/koa/FakeResponse.js
packages/server/src/utils/testing/koa/FakeResponse.js.map
packages/server/src/utils/testing/testRouters.d.ts
packages/server/src/utils/testing/testRouters.js
packages/server/src/utils/testing/testRouters.js.map
packages/server/src/utils/testing/testUtils.d.ts
packages/server/src/utils/testing/testUtils.js
packages/server/src/utils/testing/testUtils.js.map
packages/server/src/utils/time.d.ts
packages/server/src/utils/time.js
packages/server/src/utils/time.js.map
packages/server/src/utils/types.d.ts
packages/server/src/utils/types.js
packages/server/src/utils/types.js.map
packages/server/src/utils/urlUtils.d.ts
packages/server/src/utils/urlUtils.js
packages/server/src/utils/urlUtils.js.map
packages/server/src/utils/uuidgen.d.ts
packages/server/src/utils/uuidgen.js
packages/server/src/utils/uuidgen.js.map
packages/tools/generate-database-types.d.ts
packages/tools/generate-database-types.js
packages/tools/generate-database-types.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map
@@ -1660,9 +1459,6 @@ packages/tools/release-cli.js.map
packages/tools/release-electron.d.ts
packages/tools/release-electron.js
packages/tools/release-electron.js.map
packages/tools/release-ios.d.ts
packages/tools/release-ios.js
packages/tools/release-ios.js.map
packages/tools/release-server.d.ts
packages/tools/release-server.js
packages/tools/release-server.js.map

246
.gitignore vendored
View File

@@ -84,15 +84,15 @@ packages/app-cli/tests/Synchronizer.resources.js.map
packages/app-cli/tests/Synchronizer.revisions.d.ts
packages/app-cli/tests/Synchronizer.revisions.js
packages/app-cli/tests/Synchronizer.revisions.js.map
packages/app-cli/tests/Synchronizer.sharing.d.ts
packages/app-cli/tests/Synchronizer.sharing.js
packages/app-cli/tests/Synchronizer.sharing.js.map
packages/app-cli/tests/Synchronizer.tags.d.ts
packages/app-cli/tests/Synchronizer.tags.js
packages/app-cli/tests/Synchronizer.tags.js.map
packages/app-cli/tests/fsDriver.d.ts
packages/app-cli/tests/fsDriver.js
packages/app-cli/tests/fsDriver.js.map
packages/app-cli/tests/htmlUtils.d.ts
packages/app-cli/tests/htmlUtils.js
packages/app-cli/tests/htmlUtils.js.map
packages/app-cli/tests/models_Folder.d.ts
packages/app-cli/tests/models_Folder.js
packages/app-cli/tests/models_Folder.js.map
@@ -774,15 +774,18 @@ packages/lib/BaseApplication.js.map
packages/lib/BaseModel.d.ts
packages/lib/BaseModel.js
packages/lib/BaseModel.js.map
packages/lib/BaseSyncTarget.d.ts
packages/lib/BaseSyncTarget.js
packages/lib/BaseSyncTarget.js.map
packages/lib/InMemoryCache.d.ts
packages/lib/InMemoryCache.js
packages/lib/InMemoryCache.js.map
packages/lib/JoplinDatabase.d.ts
packages/lib/JoplinDatabase.js
packages/lib/JoplinDatabase.js.map
packages/lib/JoplinServerApi.d.ts
packages/lib/JoplinServerApi.js
packages/lib/JoplinServerApi.js.map
packages/lib/JoplinServerApi2.d.ts
packages/lib/JoplinServerApi2.js
packages/lib/JoplinServerApi2.js.map
packages/lib/Logger.d.ts
packages/lib/Logger.js
packages/lib/Logger.js.map
@@ -807,6 +810,9 @@ packages/lib/commands/historyForward.js.map
packages/lib/commands/synchronize.d.ts
packages/lib/commands/synchronize.js
packages/lib/commands/synchronize.js.map
packages/lib/database.d.ts
packages/lib/database.js
packages/lib/database.js.map
packages/lib/dummy.test.d.ts
packages/lib/dummy.test.js
packages/lib/dummy.test.js.map
@@ -819,15 +825,15 @@ packages/lib/eventManager.js.map
packages/lib/file-api-driver-joplinServer.d.ts
packages/lib/file-api-driver-joplinServer.js
packages/lib/file-api-driver-joplinServer.js.map
packages/lib/file-api.d.ts
packages/lib/file-api.js
packages/lib/file-api.js.map
packages/lib/fs-driver-base.d.ts
packages/lib/fs-driver-base.js
packages/lib/fs-driver-base.js.map
packages/lib/fs-driver-node.d.ts
packages/lib/fs-driver-node.js
packages/lib/fs-driver-node.js.map
packages/lib/htmlUtils.d.ts
packages/lib/htmlUtils.js
packages/lib/htmlUtils.js.map
packages/lib/import-enex-md-gen.d.ts
packages/lib/import-enex-md-gen.js
packages/lib/import-enex-md-gen.js.map
@@ -909,6 +915,9 @@ packages/lib/path-utils.js.map
packages/lib/reducer.d.ts
packages/lib/reducer.js
packages/lib/reducer.js.map
packages/lib/registry.d.ts
packages/lib/registry.js
packages/lib/registry.js.map
packages/lib/services/AlarmService.d.ts
packages/lib/services/AlarmService.js
packages/lib/services/AlarmService.js.map
@@ -1425,219 +1434,9 @@ packages/renderer/pathUtils.js.map
packages/renderer/utils.d.ts
packages/renderer/utils.js
packages/renderer/utils.js.map
packages/server/src/app.d.ts
packages/server/src/app.js
packages/server/src/app.js.map
packages/server/src/config.d.ts
packages/server/src/config.js
packages/server/src/config.js.map
packages/server/src/db.d.ts
packages/server/src/db.js
packages/server/src/db.js.map
packages/server/src/middleware/notificationHandler.d.ts
packages/server/src/middleware/notificationHandler.js
packages/server/src/middleware/notificationHandler.js.map
packages/server/src/middleware/notificationHandler.test.d.ts
packages/server/src/middleware/notificationHandler.test.js
packages/server/src/middleware/notificationHandler.test.js.map
packages/server/src/middleware/ownerHandler.d.ts
packages/server/src/middleware/ownerHandler.js
packages/server/src/middleware/ownerHandler.js.map
packages/server/src/middleware/ownerHandler.test.d.ts
packages/server/src/middleware/ownerHandler.test.js
packages/server/src/middleware/ownerHandler.test.js.map
packages/server/src/middleware/routeHandler.d.ts
packages/server/src/middleware/routeHandler.js
packages/server/src/middleware/routeHandler.js.map
packages/server/src/migrations/20190913171451_create.d.ts
packages/server/src/migrations/20190913171451_create.js
packages/server/src/migrations/20190913171451_create.js.map
packages/server/src/migrations/20203012152842_notifications.d.ts
packages/server/src/migrations/20203012152842_notifications.js
packages/server/src/migrations/20203012152842_notifications.js.map
packages/server/src/models/ApiClientModel.d.ts
packages/server/src/models/ApiClientModel.js
packages/server/src/models/ApiClientModel.js.map
packages/server/src/models/BaseModel.d.ts
packages/server/src/models/BaseModel.js
packages/server/src/models/BaseModel.js.map
packages/server/src/models/ChangeModel.d.ts
packages/server/src/models/ChangeModel.js
packages/server/src/models/ChangeModel.js.map
packages/server/src/models/ChangeModel.test.d.ts
packages/server/src/models/ChangeModel.test.js
packages/server/src/models/ChangeModel.test.js.map
packages/server/src/models/FileModel.d.ts
packages/server/src/models/FileModel.js
packages/server/src/models/FileModel.js.map
packages/server/src/models/FileModel.test.d.ts
packages/server/src/models/FileModel.test.js
packages/server/src/models/FileModel.test.js.map
packages/server/src/models/NotificationModel.d.ts
packages/server/src/models/NotificationModel.js
packages/server/src/models/NotificationModel.js.map
packages/server/src/models/NotificationModel.test.d.ts
packages/server/src/models/NotificationModel.test.js
packages/server/src/models/NotificationModel.test.js.map
packages/server/src/models/PermissionModel.d.ts
packages/server/src/models/PermissionModel.js
packages/server/src/models/PermissionModel.js.map
packages/server/src/models/SessionModel.d.ts
packages/server/src/models/SessionModel.js
packages/server/src/models/SessionModel.js.map
packages/server/src/models/UserModel.d.ts
packages/server/src/models/UserModel.js
packages/server/src/models/UserModel.js.map
packages/server/src/models/UserModel.test.d.ts
packages/server/src/models/UserModel.test.js
packages/server/src/models/UserModel.test.js.map
packages/server/src/models/factory.d.ts
packages/server/src/models/factory.js
packages/server/src/models/factory.js.map
packages/server/src/models/utils/pagination.d.ts
packages/server/src/models/utils/pagination.js
packages/server/src/models/utils/pagination.js.map
packages/server/src/models/utils/pagination.test.d.ts
packages/server/src/models/utils/pagination.test.js
packages/server/src/models/utils/pagination.test.js.map
packages/server/src/routes/api/files.d.ts
packages/server/src/routes/api/files.js
packages/server/src/routes/api/files.js.map
packages/server/src/routes/api/files.test.d.ts
packages/server/src/routes/api/files.test.js
packages/server/src/routes/api/files.test.js.map
packages/server/src/routes/api/ping.d.ts
packages/server/src/routes/api/ping.js
packages/server/src/routes/api/ping.js.map
packages/server/src/routes/api/ping.test.d.ts
packages/server/src/routes/api/ping.test.js
packages/server/src/routes/api/ping.test.js.map
packages/server/src/routes/api/sessions.d.ts
packages/server/src/routes/api/sessions.js
packages/server/src/routes/api/sessions.js.map
packages/server/src/routes/api/sessions.test.d.ts
packages/server/src/routes/api/sessions.test.js
packages/server/src/routes/api/sessions.test.js.map
packages/server/src/routes/default.d.ts
packages/server/src/routes/default.js
packages/server/src/routes/default.js.map
packages/server/src/routes/index/files.d.ts
packages/server/src/routes/index/files.js
packages/server/src/routes/index/files.js.map
packages/server/src/routes/index/home.d.ts
packages/server/src/routes/index/home.js
packages/server/src/routes/index/home.js.map
packages/server/src/routes/index/home.test.d.ts
packages/server/src/routes/index/home.test.js
packages/server/src/routes/index/home.test.js.map
packages/server/src/routes/index/login.d.ts
packages/server/src/routes/index/login.js
packages/server/src/routes/index/login.js.map
packages/server/src/routes/index/login.test.d.ts
packages/server/src/routes/index/login.test.js
packages/server/src/routes/index/login.test.js.map
packages/server/src/routes/index/logout.d.ts
packages/server/src/routes/index/logout.js
packages/server/src/routes/index/logout.js.map
packages/server/src/routes/index/logout.test.d.ts
packages/server/src/routes/index/logout.test.js
packages/server/src/routes/index/logout.test.js.map
packages/server/src/routes/index/notifications.d.ts
packages/server/src/routes/index/notifications.js
packages/server/src/routes/index/notifications.js.map
packages/server/src/routes/index/notifications.test.d.ts
packages/server/src/routes/index/notifications.test.js
packages/server/src/routes/index/notifications.test.js.map
packages/server/src/routes/index/users.d.ts
packages/server/src/routes/index/users.js
packages/server/src/routes/index/users.js.map
packages/server/src/routes/index/users.test.d.ts
packages/server/src/routes/index/users.test.js
packages/server/src/routes/index/users.test.js.map
packages/server/src/routes/oauth2/authorize.d.ts
packages/server/src/routes/oauth2/authorize.js
packages/server/src/routes/oauth2/authorize.js.map
packages/server/src/routes/routes.d.ts
packages/server/src/routes/routes.js
packages/server/src/routes/routes.js.map
packages/server/src/services/MustacheService.d.ts
packages/server/src/services/MustacheService.js
packages/server/src/services/MustacheService.js.map
packages/server/src/tools/db-migrate.d.ts
packages/server/src/tools/db-migrate.js
packages/server/src/tools/db-migrate.js.map
packages/server/src/tools/dbTools.d.ts
packages/server/src/tools/dbTools.js
packages/server/src/tools/dbTools.js.map
packages/server/src/tools/generate-types.d.ts
packages/server/src/tools/generate-types.js
packages/server/src/tools/generate-types.js.map
packages/server/src/utils/Router.d.ts
packages/server/src/utils/Router.js
packages/server/src/utils/Router.js.map
packages/server/src/utils/TransactionHandler.d.ts
packages/server/src/utils/TransactionHandler.js
packages/server/src/utils/TransactionHandler.js.map
packages/server/src/utils/auth.d.ts
packages/server/src/utils/auth.js
packages/server/src/utils/auth.js.map
packages/server/src/utils/base64.d.ts
packages/server/src/utils/base64.js
packages/server/src/utils/base64.js.map
packages/server/src/utils/cache.d.ts
packages/server/src/utils/cache.js
packages/server/src/utils/cache.js.map
packages/server/src/utils/defaultView.d.ts
packages/server/src/utils/defaultView.js
packages/server/src/utils/defaultView.js.map
packages/server/src/utils/errors.d.ts
packages/server/src/utils/errors.js
packages/server/src/utils/errors.js.map
packages/server/src/utils/htmlUtils.d.ts
packages/server/src/utils/htmlUtils.js
packages/server/src/utils/htmlUtils.js.map
packages/server/src/utils/koaIf.d.ts
packages/server/src/utils/koaIf.js
packages/server/src/utils/koaIf.js.map
packages/server/src/utils/requestUtils.d.ts
packages/server/src/utils/requestUtils.js
packages/server/src/utils/requestUtils.js.map
packages/server/src/utils/routeUtils.d.ts
packages/server/src/utils/routeUtils.js
packages/server/src/utils/routeUtils.js.map
packages/server/src/utils/routeUtils.test.d.ts
packages/server/src/utils/routeUtils.test.js
packages/server/src/utils/routeUtils.test.js.map
packages/server/src/utils/testing/apiUtils.d.ts
packages/server/src/utils/testing/apiUtils.js
packages/server/src/utils/testing/apiUtils.js.map
packages/server/src/utils/testing/koa/FakeCookies.d.ts
packages/server/src/utils/testing/koa/FakeCookies.js
packages/server/src/utils/testing/koa/FakeCookies.js.map
packages/server/src/utils/testing/koa/FakeRequest.d.ts
packages/server/src/utils/testing/koa/FakeRequest.js
packages/server/src/utils/testing/koa/FakeRequest.js.map
packages/server/src/utils/testing/koa/FakeResponse.d.ts
packages/server/src/utils/testing/koa/FakeResponse.js
packages/server/src/utils/testing/koa/FakeResponse.js.map
packages/server/src/utils/testing/testRouters.d.ts
packages/server/src/utils/testing/testRouters.js
packages/server/src/utils/testing/testRouters.js.map
packages/server/src/utils/testing/testUtils.d.ts
packages/server/src/utils/testing/testUtils.js
packages/server/src/utils/testing/testUtils.js.map
packages/server/src/utils/time.d.ts
packages/server/src/utils/time.js
packages/server/src/utils/time.js.map
packages/server/src/utils/types.d.ts
packages/server/src/utils/types.js
packages/server/src/utils/types.js.map
packages/server/src/utils/urlUtils.d.ts
packages/server/src/utils/urlUtils.js
packages/server/src/utils/urlUtils.js.map
packages/server/src/utils/uuidgen.d.ts
packages/server/src/utils/uuidgen.js
packages/server/src/utils/uuidgen.js.map
packages/tools/generate-database-types.d.ts
packages/tools/generate-database-types.js
packages/tools/generate-database-types.js.map
packages/tools/lerna-add.d.ts
packages/tools/lerna-add.js
packages/tools/lerna-add.js.map
@@ -1647,9 +1446,6 @@ packages/tools/release-cli.js.map
packages/tools/release-electron.d.ts
packages/tools/release-electron.js
packages/tools/release-electron.js.map
packages/tools/release-ios.d.ts
packages/tools/release-ios.js
packages/tools/release-ios.js.map
packages/tools/release-server.d.ts
packages/tools/release-server.js
packages/tools/release-server.js.map

View File

@@ -162,7 +162,15 @@ DESKTOP=${DESKTOP,,} # convert to lower case
#-----------------------------------------------------
echo 'Create Desktop icon...'
if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cinnamon.*|.*deepin.*|.*pantheon.*|.*lxde.*|.*i3.*|.*sway.* ]]
# Initially only desktop environments that were confirmed to use desktop files stored in
# `.local/share/desktop` had a desktop file created.
# However some environments don't return a desktop BUT still support these desktop files
# the command check was added to support all Desktops that have support for the
# freedesktop standard
# The old checks are left in place for historical reasons, but
# NO MORE DESKTOP ENVIRONMENTS SHOULD BE ADDED
# If a new environment needs to be supported, then the command check section should be re-thought
if [[ $DESKTOP =~ .*gnome.*|.*kde.*|.*xfce.*|.*mate.*|.*lxqt.*|.*unity.*|.*x-cinnamon.*|.*deepin.*|.*pantheon.*|.*lxde.*|.*i3.*|.*sway.* ]] || [[ `command -v update-desktop-database` ]]
then
# Only delete the desktop file if it will be replaced
rm -f ~/.local/share/applications/appimagekit-joplin.desktop

View File

@@ -34,7 +34,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v1.6.8/Jo
Operating System | Download | Alt. Download
---|---|---
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.7.3/joplin-v1.7.3.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.7.3/joplin-v1.7.3-32bit.apk)
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.6.7/joplin-v1.6.7.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v1.6.7/joplin-v1.6.7-32bit.apk)
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
## Terminal application
@@ -117,6 +117,7 @@ The Web Clipper is a browser extension that allows you to save web pages and scr
- [Search Sorting spec](https://github.com/laurent22/joplin/blob/dev/readme/spec/search_sorting.md)
- [Server: File URL Format](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_file_url_format.md)
- [Server: Delta Sync](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_delta_sync.md)
- [Server: Sharing](https://github.com/laurent22/joplin/blob/dev/readme/spec/server_sharing.md)
- Google Summer of Code 2020

View File

@@ -8,8 +8,8 @@
"license": "MIT",
"scripts": {
"bootstrap": "lerna bootstrap --no-ci",
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
"bootstrapServerOnly": "lerna bootstrap --no-ci --include-dependents --include-dependencies --scope @joplin/server",
"bootstrapIgnoreScripts": "lerna bootstrap --ignore-scripts --no-ci",
"build": "lerna run build && npm run tsc",
"buildApiDoc": "npm start --prefix=packages/app-cli -- apidoc ../../readme/api/references/rest_api.md",
"buildDoc": "./packages/tools/build-all.sh",
@@ -18,8 +18,8 @@
"buildTranslationsNoTsc": "node packages/tools/build-translation.js",
"buildWebsite": "npm run buildApiDoc && node ./packages/tools/build-website.js && npm run buildPluginDoc",
"circularDependencyCheck": "madge --warning --circular --extensions js ./",
"clean": "lerna clean -y && lerna run clean",
"dependencyTree": "madge",
"clean": "lerna clean -y && lerna run clean",
"generateDatabaseTypes": "node packages/tools/generate-database-types",
"linkChecker": "linkchecker https://joplinapp.org",
"linter-ci": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
@@ -27,12 +27,11 @@
"linter": "./node_modules/.bin/eslint --resolve-plugins-relative-to . --fix --quiet --ext .js --ext .jsx --ext .ts --ext .tsx",
"postinstall": "npm run bootstrap --no-ci && npm run build",
"publishAll": "git pull && npm run build && lerna version --yes --no-private --no-git-tag-version && gulp completePublishAll",
"releaseAndroid": "export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
"releaseAndroidClean": "node packages/tools/release-android.js",
"releaseAndroid": "export PATH=\"/usr/local/opt/openjdk@11/bin:$PATH\" && node packages/tools/release-android.js",
"releaseCli": "node packages/tools/release-cli.js",
"releaseClipper": "node packages/tools/release-clipper.js",
"releaseDesktop": "node packages/tools/release-electron.js",
"releaseIOS": "node packages/tools/release-ios.js",
"releasePluginGenerator": "node packages/tools/release-plugin-generator.js",
"releaseServer": "node packages/tools/release-server.js",
"setupNewRelease": "node ./packages/tools/setupNewRelease",

View File

@@ -4,7 +4,7 @@ const fs = require('fs-extra');
const Logger = require('@joplin/lib/Logger').default;
const { dirname } = require('@joplin/lib/path-utils');
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
const JoplinDatabase = require('@joplin/lib/JoplinDatabase').default;
const BaseModel = require('@joplin/lib/BaseModel').default;
const Folder = require('@joplin/lib/models/Folder').default;
const Note = require('@joplin/lib/models/Note').default;

View File

@@ -4,7 +4,7 @@ const BaseModel = require('@joplin/lib/BaseModel').default;
const { toTitleCase } = require('@joplin/lib/string-utils.js');
const { reg } = require('@joplin/lib/registry.js');
const markdownUtils = require('@joplin/lib/markdownUtils').default;
const { Database } = require('@joplin/lib/database.js');
const Database = require('@joplin/lib/database').default;
const shim = require('@joplin/lib/shim').default;
class Command extends BaseCommand {

View File

@@ -2,7 +2,7 @@ const { BaseCommand } = require('./base-command.js');
const { app } = require('./app.js');
const { _ } = require('@joplin/lib/locale');
const BaseModel = require('@joplin/lib/BaseModel').default;
const { Database } = require('@joplin/lib/database.js');
const Database = require('@joplin/lib/database').default;
const Note = require('@joplin/lib/models/Note').default;
class Command extends BaseCommand {

View File

@@ -0,0 +1,39 @@
import { afterAllCleanUp, synchronizerStart, setupDatabaseAndSynchronizer, switchClient } from './test-utils';
import Note from '@joplin/lib/models/Note';
import BaseItem from '@joplin/lib/models/BaseItem';
import shim from '@joplin/lib/shim';
import Resource from '@joplin/lib/models/Resource';
describe('Synchronizer.sharing', function() {
beforeEach(async (done) => {
await setupDatabaseAndSynchronizer(1);
await setupDatabaseAndSynchronizer(2);
await switchClient(1);
done();
});
afterAll(async () => {
await afterAllCleanUp();
});
it('should mark link resources as shared before syncing', (async () => {
let note1 = await Note.save({ title: 'note1' });
note1 = await shim.attachFileToNote(note1, `${__dirname}/../tests/support/photo.jpg`);
const resourceId1 = (await Note.linkedResourceIds(note1.body))[0];
const note2 = await Note.save({ title: 'note2' });
await shim.attachFileToNote(note2, `${__dirname}/../tests/support/photo.jpg`);
expect((await Resource.sharedResourceIds()).length).toBe(0);
await BaseItem.updateShareStatus(note1, true);
await synchronizerStart();
const sharedResourceIds = await Resource.sharedResourceIds();
expect(sharedResourceIds.length).toBe(1);
expect(sharedResourceIds[0]).toBe(resourceId1);
}));
});

View File

@@ -1,4 +1,6 @@
import htmlUtils from '@joplin/lib/htmlUtils';
/* eslint-disable no-unused-vars */
const htmlUtils = require('@joplin/lib/htmlUtils.js');
describe('htmlUtils', function() {
@@ -17,8 +19,8 @@ describe('htmlUtils', function() {
];
for (let i = 0; i < testCases.length; i++) {
const md = testCases[i][0] as string;
const expected = testCases[i][1] as string[];
const md = testCases[i][0];
const expected = testCases[i][1];
expect(htmlUtils.extractImageUrls(md).join(' ')).toBe(expected.join(' '));
}
@@ -31,19 +33,19 @@ describe('htmlUtils', function() {
['<img src="http://test.com/img.png" alt="testing" >', ['http://other.com/img.png'], '<img src="http://other.com/img.png" alt="testing" >'],
];
const callback = (urls: string[]) => {
const callback = (urls) => {
let i = -1;
return function(_src: string) {
return function(src) {
i++;
return urls[i];
};
};
for (let i = 0; i < testCases.length; i++) {
const md = testCases[i][0] as string;
const r = htmlUtils.replaceImageUrls(md, callback(testCases[i][1] as string[]));
expect(r.trim()).toBe((testCases[i][2] as string).trim());
const md = testCases[i][0];
const r = htmlUtils.replaceImageUrls(md, callback(testCases[i][1]));
expect(r.trim()).toBe(testCases[i][2].trim());
}
}));

View File

@@ -6,7 +6,7 @@ const { fileContentEqual, setupDatabase, setupDatabaseAndSynchronizer, db, synch
const Folder = require('@joplin/lib/models/Folder').default;
const Note = require('@joplin/lib/models/Note').default;
const Tag = require('@joplin/lib/models/Tag').default;
const { Database } = require('@joplin/lib/database.js');
const Database = require('@joplin/lib/database').default;
const Setting = require('@joplin/lib/models/Setting').default;
const BaseItem = require('@joplin/lib/models/BaseItem').default;
const BaseModel = require('@joplin/lib/BaseModel').default;

View File

@@ -18,9 +18,9 @@ import PluginService from '@joplin/lib/services/plugins/PluginService';
import FileApiDriverJoplinServer from '@joplin/lib/file-api-driver-joplinServer';
import OneDriveApi from '@joplin/lib/onedrive-api';
import SyncTargetOneDrive from '@joplin/lib/SyncTargetOneDrive';
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
const fs = require('fs-extra');
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
import Folder from '@joplin/lib/models/Folder';
import Note from '@joplin/lib/models/Note';
@@ -52,7 +52,7 @@ import RevisionService from '@joplin/lib/services/RevisionService';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
const WebDavApi = require('@joplin/lib/WebDavApi');
const DropboxApi = require('@joplin/lib/DropboxApi');
import JoplinServerApi from '@joplin/lib/JoplinServerApi2';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
const { loadKeychainServiceAndSettings } = require('@joplin/lib/services/SettingUtils');
const md5 = require('md5');
const S3 = require('aws-sdk/clients/s3');
@@ -402,7 +402,7 @@ async function setupDatabaseAndSynchronizer(id: number, options: any = null) {
await fileApi().clearRoot();
}
function db(id: number = null) {
function db(id: number = null): JoplinDatabase {
if (id === null) id = currentClient_;
return databases_[id];
}

View File

@@ -31,7 +31,7 @@ import MasterKey from '@joplin/lib/models/MasterKey';
import Folder from '@joplin/lib/models/Folder';
const fs = require('fs-extra');
import Tag from '@joplin/lib/models/Tag';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const packageInfo = require('./packageInfo.js');
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';
const ClipperServer = require('@joplin/lib/ClipperServer');
@@ -704,7 +704,7 @@ class Application extends BaseApplication {
if (Setting.value('env') === 'dev') {
void AlarmService.updateAllNotifications();
} else {
reg.scheduleSync(1000).then(() => {
void reg.scheduleSync(1000).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();

View File

@@ -42,7 +42,7 @@ class ConfigScreenComponent extends React.Component<any, any> {
this.sidebar_selectionChange = this.sidebar_selectionChange.bind(this);
this.checkSyncConfig_ = this.checkSyncConfig_.bind(this);
this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
// this.checkNextcloudAppButton_click = this.checkNextcloudAppButton_click.bind(this);
this.showLogButton_click = this.showLogButton_click.bind(this);
this.nextcloudAppHelpLink_click = this.nextcloudAppHelpLink_click.bind(this);
this.onCancelClick = this.onCancelClick.bind(this);
@@ -57,10 +57,10 @@ class ConfigScreenComponent extends React.Component<any, any> {
await shared.checkSyncConfig(this, this.state.settings);
}
async checkNextcloudAppButton_click() {
this.setState({ showNextcloudAppLog: true });
await shared.checkNextcloudApp(this, this.state.settings);
}
// async checkNextcloudAppButton_click() {
// this.setState({ showNextcloudAppLog: true });
// await shared.checkNextcloudApp(this, this.state.settings);
// }
showLogButton_click() {
this.setState({ showNextcloudAppLog: true });
@@ -203,48 +203,48 @@ class ConfigScreenComponent extends React.Component<any, any> {
);
}
if (syncTargetMd.name === 'nextcloud') {
const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
// if (syncTargetMd.name === 'nextcloud') {
// const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
let status = _('Unknown');
let errorMessage = null;
// let status = _('Unknown');
// let errorMessage = null;
if (this.state.checkNextcloudAppResult === 'checking') {
status = _('Checking...');
} else if (syncTarget) {
if (syncTarget.uuid) status = _('OK');
if (syncTarget.error) {
status = _('Error');
errorMessage = syncTarget.error;
}
}
// if (this.state.checkNextcloudAppResult === 'checking') {
// status = _('Checking...');
// } else if (syncTarget) {
// if (syncTarget.uuid) status = _('OK');
// if (syncTarget.error) {
// status = _('Error');
// errorMessage = syncTarget.error;
// }
// }
const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
<div style={statusStyle}>
<p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
<pre>{errorMessage}</pre>
</div>
);
// const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
// <div style={statusStyle}>
// <p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
// <pre>{errorMessage}</pre>
// </div>
// );
const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
<a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
);
// const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
// <a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
// );
const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
// const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
settingComps.push(
<div key="nextcloud_app_check" style={this.rowStyle_}>
<span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
&nbsp;&nbsp;
{showLogButton}
&nbsp;&nbsp;
<Button level={ButtonLevel.Secondary} style={{ display: 'inline-block' }} title={_('Check Status')} disabled={this.state.checkNextcloudAppResult === 'checking'} onClick={this.checkNextcloudAppButton_click}/>
&nbsp;&nbsp;
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
{statusComp}
</div>
);
}
// settingComps.push(
// <div key="nextcloud_app_check" style={this.rowStyle_}>
// <span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
// &nbsp;&nbsp;
// {showLogButton}
// &nbsp;&nbsp;
// <Button level={ButtonLevel.Secondary} style={{ display: 'inline-block' }} title={_('Check Status')} disabled={this.state.checkNextcloudAppResult === 'checking'} onClick={this.checkNextcloudAppButton_click}/>
// &nbsp;&nbsp;
// <a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
// {statusComp}
// </div>
// );
// }
}
let advancedSettingsButton = null;

View File

@@ -114,7 +114,7 @@ export default function(props: Props) {
let cancelled = false;
async function fetchPluginIds() {
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems.map(p => p.manifest));
const pluginIds = await repoApi().canBeUpdatedPlugins(pluginItems as any);
if (cancelled) return;
const conv: Record<string, boolean> = {};
pluginIds.forEach(id => conv[id] = true);

View File

@@ -19,7 +19,7 @@ import stateToWhenClauseContext from '../services/commands/stateToWhenClauseCont
import bridge from '../services/bridge';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const packageInfo = require('../packageInfo.js');
const { clipboard } = require('electron');
const Menu = bridge().Menu;

View File

@@ -30,7 +30,7 @@ const { clipboard } = require('electron');
const shared = require('@joplin/lib/components/shared/note-screen-shared.js');
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const menuUtils = new MenuUtils(CommandService.instance());
@@ -371,7 +371,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
/* These must be important to prevent the codemirror defaults from taking over*/
.CodeMirror {
font-family: monospace;
font-size: ${theme.editorFontSize}px;
font-size: ${props.fontSize}px;
height: 100% !important;
width: 100% !important;
color: inherit !important;

View File

@@ -31,7 +31,7 @@ import Setting from '@joplin/lib/models/Setting';
// import eventManager from '@joplin/lib/eventManager';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
// Based on http://pypl.github.io/PYPL.html
const topLanguages = [

View File

@@ -2,7 +2,7 @@ import { NoteBodyEditorProps } from '../../../utils/types';
const { buildStyle } = require('@joplin/lib/theme');
export default function styles(props: NoteBodyEditorProps) {
return buildStyle('CodeMirror', props.themeId, (theme: any) => {
return buildStyle(['CodeMirror', props.fontSize], props.themeId, (theme: any) => {
return {
root: {
position: 'relative',
@@ -49,8 +49,8 @@ export default function styles(props: NoteBodyEditorProps) {
flex: 1,
overflowY: 'hidden',
paddingTop: 0,
lineHeight: `${theme.textAreaLineHeight}px`,
fontSize: `${theme.editorFontSize}px`,
lineHeight: `${Math.round(17 * props.fontSize / 12)}px`,
fontSize: `${props.fontSize}px`,
color: theme.color,
backgroundColor: theme.backgroundColor,
codeMirrorTheme: theme.codeMirrorTheme, // Defined in theme.js

View File

@@ -5,7 +5,7 @@ import { extname } from 'path';
import shim from '@joplin/lib/shim';
import uuid from '@joplin/lib/uuid';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
export default function useExternalPlugins(CodeMirror: any, plugins: PluginStates) {

View File

@@ -3,7 +3,7 @@ import CommandService from '@joplin/lib/services/CommandService';
import KeymapService, { KeymapItem } from '@joplin/lib/services/KeymapService';
import { EditorCommand } from '../../../utils/types';
import shim from '@joplin/lib/shim';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
export default function useKeymap(CodeMirror: any) {

View File

@@ -16,7 +16,7 @@ import shim from '@joplin/lib/shim';
const { MarkupToHtml } = require('@joplin/renderer');
const taboverride = require('taboverride');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
import BaseItem from '@joplin/lib/models/BaseItem';
const { themeStyle } = require('@joplin/lib/theme');
const { clipboard } = require('electron');

View File

@@ -34,7 +34,7 @@ import ExternalEditWatcher from '@joplin/lib/services/ExternalEditWatcher';
const { themeStyle } = require('@joplin/lib/theme');
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
const NoteSearchBar = require('../NoteSearchBar.min.js');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
import Note from '@joplin/lib/models/Note';
import Folder from '@joplin/lib/models/Folder';
const bridge = require('electron').remote.require('./bridge').default;
@@ -399,6 +399,7 @@ function NoteEditor(props: NoteEditorProps) {
onDrop: onDrop,
noteToolbarButtonInfos: props.toolbarButtonInfos,
plugins: props.plugins,
fontSize: Setting.value('style.editor.fontSize'),
};
let editor = null;

View File

@@ -5,7 +5,6 @@ const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import Resource from '@joplin/lib/models/Resource';
import htmlUtils from '@joplin/lib/htmlUtils';
const fs = require('fs-extra');
const { clipboard } = require('electron');
const { toSystemSlashes } = require('@joplin/lib/path-utils');
@@ -48,13 +47,7 @@ function handleCopyToClipboard(options: ContextMenuOptions) {
if (options.textToCopy) {
clipboard.writeText(options.textToCopy);
} else if (options.htmlToCopy) {
// In that case we need to set both HTML and Text context, otherwise it
// won't be possible to paste the text in, for example, a text editor.
// https://github.com/laurent22/joplin/issues/4441
clipboard.write({
text: htmlUtils.stripHtml(options.htmlToCopy),
html: options.htmlToCopy,
});
clipboard.writeHTML(options.htmlToCopy);
}
}

View File

@@ -5,7 +5,7 @@ import BaseModel from '@joplin/lib/BaseModel';
import Resource from '@joplin/lib/models/Resource';
const bridge = require('electron').remote.require('./bridge').default;
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const joplinRendererUtils = require('@joplin/renderer').utils;
const { clipboard } = require('electron');
const mimeUtils = require('@joplin/lib/mime-utils.js').mime;

View File

@@ -64,6 +64,7 @@ export interface NoteBodyEditorProps {
onDrop: Function;
noteToolbarButtonInfos: ToolbarButtonInfo[];
plugins: PluginStates;
fontSize: number;
}
export interface FormNote {

View File

@@ -10,7 +10,7 @@ import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index'
const { MarkupToHtml } = require('@joplin/renderer');
import Note from '@joplin/lib/models/Note';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
import DecryptionWorker from '@joplin/lib/services/DecryptionWorker';

View File

@@ -12,7 +12,7 @@ const bridge = require('electron').remote.require('./bridge').default;
const { urlDecode } = require('@joplin/lib/string-utils');
const urlUtils = require('@joplin/lib/urlUtils');
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
export default function useMessageHandler(scrollWhenReady: any, setScrollWhenReady: Function, editorRef: any, setLocalSearchResultCount: Function, dispatch: Function, formNote: FormNote) {
return useCallback(async (event: any) => {

View File

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { FormNote, ScrollOptionTypes } from './types';
import CommandService, { CommandDeclaration, CommandRuntime, CommandContext } from '@joplin/lib/services/CommandService';
import time from '@joplin/lib/time';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const commandsWithDependencies = [
require('../commands/showLocalSearch'),

View File

@@ -1,7 +1,7 @@
import PostMessageService, { MessageResponse, ResponderComponentType } from '@joplin/lib/services/PostMessageService';
import * as React from 'react';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
interface Props {
onDomReady: Function;

View File

@@ -3,7 +3,7 @@ import ButtonBar from './ConfigScreen/ButtonBar';
import { _ } from '@joplin/lib/locale';
const { connect } = require('react-redux');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
import Setting from '@joplin/lib/models/Setting';
const bridge = require('electron').remote.require('./bridge').default;
const { themeStyle } = require('@joplin/lib/theme');
@@ -44,7 +44,7 @@ class OneDriveLoginScreenComponent extends React.Component<any, any> {
if (!auth) {
log(_('Authentication was not completed (did not receive an authentication token).'));
} else {
reg.scheduleSync(0);
void reg.scheduleSync(0);
}
}

View File

@@ -1,14 +1,15 @@
import * as React from 'react';
import { useState, useEffect } from 'react';
import JoplinServerApi from '@joplin/lib/JoplinServerApi';
import { _, _n } from '@joplin/lib/locale';
const { themeStyle, buildStyle } = require('@joplin/lib/theme');
const DialogButtonRow = require('./DialogButtonRow.min');
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
import BaseItem from '@joplin/lib/models/BaseItem';
const { reg } = require('@joplin/lib/registry.js');
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
const { themeStyle, buildStyle } = require('@joplin/lib/theme');
const DialogButtonRow = require('./DialogButtonRow.min');
import { reg } from '@joplin/lib/registry';
const { clipboard } = require('electron');
interface ShareNoteDialogProps {
@@ -82,17 +83,22 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
void fetchNotes();
}, [props.noteIds]);
const appApi = async () => {
return reg.syncTargetNextcloud().appApi();
const fileApi = async () => {
const syncTarget = reg.syncTarget() as SyncTargetJoplinServer;
return syncTarget.fileApi();
};
const joplinServerApi = async (): Promise<JoplinServerApi> => {
return (await fileApi()).driver().api();
};
const buttonRow_click = () => {
props.onClose();
};
const copyLinksToClipboard = (shares: SharesMap) => {
const copyLinksToClipboard = (api: JoplinServerApi, shares: SharesMap) => {
const links = [];
for (const n in shares) links.push(shares[n]._url);
for (const n in shares) links.push(api.shareUrl(shares[n]));
clipboard.writeText(links.join('\n'));
};
@@ -110,17 +116,15 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
setSharesState('creating');
const api = await appApi();
const syncTargetId = api.syncTargetId(Setting.toPlainObject());
const api = await joplinServerApi();
const newShares = Object.assign({}, shares);
let sharedStatusChanged = false;
for (const note of notes) {
const result = await api.exec('POST', 'shares', {
syncTargetId: syncTargetId,
noteId: note.id,
});
newShares[note.id] = result;
const fullPath = (await fileApi()).fullPath(BaseItem.systemPath(note.id));
const share = await api.shareFile(fullPath);
newShares[note.id] = share;
const changed = await BaseItem.updateShareStatus(note, true);
if (changed) sharedStatusChanged = true;
@@ -134,7 +138,7 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
setSharesState('creating');
}
copyLinksToClipboard(newShares);
copyLinksToClipboard(api, newShares);
setSharesState('created');
} catch (error) {
@@ -193,7 +197,14 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
return '';
};
const encryptionWarningMessage = !Setting.value('encryption.enabled') ? null : <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}</div>;
function renderEncryptionWarningMessage() {
if (!Setting.value('encryption.enabled')) return null;
return <div style={theme.textStyle}>{_('Note: When a note is shared, it will no longer be encrypted on the server.')}<hr/></div>;
}
function renderBetaWarningMessage() {
return <div style={theme.textStyle}>{'Sharing notes via Joplin Server is a Beta feature and the API might change later on. What it means is that if you share a note, the link might become invalid after an upgrade, and you will have to share it again.'}</div>;
}
const rootStyle = Object.assign({}, theme.dialogBox);
rootStyle.width = '50%';
@@ -205,7 +216,8 @@ export default function ShareNoteDialog(props: ShareNoteDialogProps) {
{renderNoteList(notes)}
<button disabled={['creating', 'synchronizing'].indexOf(sharesState) >= 0} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
{encryptionWarningMessage}
{renderEncryptionWarningMessage()}
{renderBetaWarningMessage()}
<DialogButtonRow themeId={props.themeId} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { utils as pluginUtils, PluginStates } from '@joplin/lib/services/plugins/reducer';
import CommandService from '@joplin/lib/services/CommandService';
import SyncTargetJoplinServer from '@joplin/lib/SyncTargetJoplinServer';
import eventManager from '@joplin/lib/eventManager';
import InteropService from '@joplin/lib/services/interop/InteropService';
import MenuUtils from '@joplin/lib/services/commands/MenuUtils';
@@ -12,6 +13,7 @@ const bridge = require('electron').remote.require('./bridge').default;
const Menu = bridge().Menu;
const MenuItem = bridge().MenuItem;
import Note from '@joplin/lib/models/Note';
import Setting from '@joplin/lib/models/Setting';
const { substrWithEllipsis } = require('@joplin/lib/string-utils');
interface ContextMenuProps {
@@ -131,11 +133,13 @@ export default class NoteListUtils {
})
);
menu.append(
new MenuItem(
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())
)
);
if (Setting.value('sync.target') === SyncTargetJoplinServer.id()) {
menu.append(
new MenuItem(
menuUtils.commandToStatefulMenuItem('showShareNoteDialog', noteIds.slice())
)
);
}
const exportMenu = new Menu();

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.7.10",
"version": "1.7.9",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "1.7.10",
"version": "1.7.9",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -141,8 +141,8 @@ android {
applicationId "net.cozic.joplin"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2097624
versionName "1.7.3"
versionCode 2097622
versionName "1.7.1"
ndk {
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}

View File

@@ -10,7 +10,7 @@ const { View } = require('react-native');
const { WebView } = require('react-native-webview');
const { themeStyle } = require('../global-style.js');
import BackButtonDialogBox from '../BackButtonDialogBox';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
interface Props {
themeId: number;

View File

@@ -4,7 +4,7 @@ import shim from '@joplin/lib/shim';
const { ToastAndroid } = require('react-native');
const { _ } = require('@joplin/lib/locale.js');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const { dialogs } = require('../../../utils/dialogs.js');
import Resource from '@joplin/lib/models/Resource';
const Share = require('react-native-share').default;

View File

@@ -29,7 +29,7 @@ const NoteTagsDialog = require('./NoteTagsDialog');
import time from '@joplin/lib/time';
const { Checkbox } = require('../checkbox.js');
const { _ } = require('@joplin/lib/locale');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle, editorFont } = require('../global-style.js');

View File

@@ -10,7 +10,7 @@ const { NoteItem } = require('../note-item.js');
const { BaseScreenComponent } = require('../base-screen.js');
const { themeStyle } = require('../global-style.js');
const DialogBox = require('react-native-dialogbox').default;
const SearchEngineUtils = require('@joplin/lib/services/searchengine/SearchEngineUtils').default;
const SearchEngineUtils = require('@joplin/lib/services/searchengine/SearchEngineUtils');
const SearchEngine = require('@joplin/lib/services/searchengine/SearchEngine').default;
Icon.loadFont();

View File

@@ -338,13 +338,13 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 62;
CURRENT_PROJECT_VERSION = 61;
DEVELOPMENT_TEAM = A9BXAFS6CT;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 10.7.1;
MARKETING_VERSION = 10.7.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
@@ -365,12 +365,12 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = 62;
CURRENT_PROJECT_VERSION = 61;
DEVELOPMENT_TEAM = A9BXAFS6CT;
INFOPLIST_FILE = Joplin/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 10.7.1;
MARKETING_VERSION = 10.7.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",

View File

@@ -50,8 +50,8 @@ import BaseItem from '@joplin/lib/models/BaseItem';
import MasterKey from '@joplin/lib/models/MasterKey';
import Revision from '@joplin/lib/models/Revision';
import RevisionService from '@joplin/lib/services/RevisionService';
const { JoplinDatabase } = require('@joplin/lib/joplin-database.js');
const { Database } = require('@joplin/lib/database.js');
import JoplinDatabase from '@joplin/lib/JoplinDatabase';
import Database from '@joplin/lib/database';
const { NotesScreen } = require('./components/screens/notes.js');
const { TagsScreen } = require('./components/screens/tags.js');
const { ConfigScreen } = require('./components/screens/config.js');
@@ -67,7 +67,7 @@ const { SideMenu } = require('./components/side-menu.js');
const { SideMenuContent } = require('./components/side-menu-content.js');
const { SideMenuContentNote } = require('./components/side-menu-content-note.js');
const { DatabaseDriverReactNative } = require('./utils/database-driver-react-native');
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
const { defaultState } = require('@joplin/lib/reducer');
const { FileApiDriverLocal } = require('@joplin/lib/file-api-driver-local.js');
import ResourceFetcher from '@joplin/lib/services/ResourceFetcher';
@@ -118,7 +118,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
if (action.type == 'NAV_GO') Keyboard.dismiss();
if (['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!await reg.syncTarget().syncStarted()) reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
if (!await reg.syncTarget().syncStarted()) void reg.scheduleSync(5 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
SearchEngine.instance().scheduleSyncTables();
}
@@ -151,7 +151,7 @@ const generalMiddleware = (store: any) => (next: any) => async (action: any) =>
// Schedule a sync operation so that items that need to be encrypted
// are sent to sync target.
reg.scheduleSync();
void reg.scheduleSync();
}
if (action.type == 'NAV_GO' && action.routeName == 'Notes') {
@@ -427,7 +427,7 @@ async function initialize(dispatch: Function) {
db.setLogger(dbLogger);
reg.setDb(db);
reg.dispatch = dispatch;
// reg.dispatch = dispatch;
BaseModel.dispatch = dispatch;
FoldersScreenUtils.dispatch = dispatch;
BaseSyncTarget.dispatch = dispatch;
@@ -585,7 +585,7 @@ async function initialize(dispatch: Function) {
// When the app starts we want the full sync to
// start almost immediately to get the latest data.
reg.scheduleSync(1000).then(() => {
void reg.scheduleSync(1000).then(() => {
// Wait for the first sync before updating the notifications, since synchronisation
// might change the notifications.
void AlarmService.updateAllNotifications();
@@ -672,7 +672,7 @@ class AppComponent extends React.Component {
if (this.props.selectedFolderId) {
await handleShared(sharedData, this.props.selectedFolderId, this.props.dispatch);
} else {
reg.logger.info('Cannot handle share - default folder id is not set');
reg.logger().info('Cannot handle share - default folder id is not set');
}
}
}

View File

@@ -5,7 +5,7 @@ import * as QuickActions from 'react-native-quick-actions';
import { _ } from '@joplin/lib/locale';
const { DeviceEventEmitter } = require('react-native');
import Note from '@joplin/lib/models/Note';
const { reg } = require('@joplin/lib/registry.js');
import { reg } from '@joplin/lib/registry';
type TData = {
type: string;

View File

@@ -11,7 +11,7 @@ import SyncTargetOneDrive from './SyncTargetOneDrive';
const { createStore, applyMiddleware } = require('redux');
const { defaultState, stateUtils } = require('./reducer');
const { JoplinDatabase } = require('./joplin-database.js');
import JoplinDatabase from './JoplinDatabase';
const { FoldersScreenUtils } = require('./folders-screen-utils.js');
const { DatabaseDriverNode } = require('./database-driver-node.js');
import BaseModel from './BaseModel';
@@ -20,9 +20,9 @@ import BaseItem from './models/BaseItem';
import Note from './models/Note';
import Tag from './models/Tag';
const { splitCommandString } = require('./string-utils.js');
const { reg } = require('./registry.js');
import { reg } from './registry';
import time from './time';
const BaseSyncTarget = require('./BaseSyncTarget.js');
import BaseSyncTarget from './BaseSyncTarget';
const reduxSharedMiddleware = require('./components/shared/reduxSharedMiddleware');
const os = require('os');
const fs = require('fs-extra');
@@ -433,7 +433,7 @@ export default class BaseApplication {
// Schedule a sync operation so that items that need to be encrypted
// are sent to sync target.
reg.scheduleSync();
void reg.scheduleSync();
}
},
'sync.interval': async () => {
@@ -470,7 +470,7 @@ export default class BaseApplication {
await reduxSharedMiddleware(store, next, action);
if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
if (!(await reg.syncTarget().syncStarted())) reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
if (!(await reg.syncTarget().syncStarted())) void reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
SearchEngine.instance().scheduleSyncTables();
}
@@ -604,7 +604,7 @@ export default class BaseApplication {
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn()));
BaseModel.dispatch = this.store().dispatch;
FoldersScreenUtils.dispatch = this.store().dispatch;
reg.dispatch = this.store().dispatch;
// reg.dispatch = this.store().dispatch;
BaseSyncTarget.dispatch = this.store().dispatch;
DecryptionWorker.instance().dispatch = this.store().dispatch;
ResourceFetcher.instance().dispatch = this.store().dispatch;
@@ -614,7 +614,7 @@ export default class BaseApplication {
this.store_ = null;
BaseModel.dispatch = function() {};
FoldersScreenUtils.dispatch = function() {};
reg.dispatch = function() {};
// reg.dispatch = function() {};
BaseSyncTarget.dispatch = function() {};
DecryptionWorker.instance().dispatch = function() {};
ResourceFetcher.instance().dispatch = function() {};
@@ -720,8 +720,8 @@ export default class BaseApplication {
reg.setLogger(Logger.create(''));
reg.dispatch = () => {};
reg.setLogger(Logger.create('') as Logger);
// reg.dispatch = () => {};
BaseService.logger_ = globalLogger;

View File

@@ -1,8 +1,9 @@
import paginationToSql from './models/utils/paginationToSql';
const { Database } = require('./database.js');
import Database from './database';
import uuid from './uuid';
import time from './time';
import JoplinDatabase from './JoplinDatabase';
const Mutex = require('async-mutex').Mutex;
// New code should make use of this enum
@@ -69,7 +70,7 @@ class BaseModel {
public static dispatch: Function = function() {};
private static saveMutexes_: any = {};
private static db_: any;
private static db_: JoplinDatabase;
static modelType(): ModelType {
throw new Error('Must be overriden');
@@ -631,12 +632,12 @@ class BaseModel {
return this.db().exec(`DELETE FROM ${this.tableName()} WHERE id = ?`, [id]);
}
static batchDelete(ids: string[], options: any = null) {
static async batchDelete(ids: string[], options: any = null) {
if (!ids.length) return;
options = this.modOptions(options);
const idFieldName = options.idFieldName ? options.idFieldName : 'id';
const sql = `DELETE FROM ${this.tableName()} WHERE ${idFieldName} IN ("${ids.join('","')}")`;
return this.db().exec(sql);
await this.db().exec(sql);
}
static db() {

View File

@@ -1,88 +1,94 @@
const EncryptionService = require('./services/EncryptionService').default;
const shim = require('./shim').default;
import Logger from './Logger';
import Synchronizer from './Synchronizer';
import EncryptionService from './services/EncryptionService';
import shim from './shim';
import ResourceService from './services/ResourceService';
class BaseSyncTarget {
constructor(db, options = null) {
export default class BaseSyncTarget {
public static dispatch: Function = () => {};
private synchronizer_: Synchronizer = null;
private initState_: any = null;
private logger_: Logger = null;
private options_: any;
private db_: any;
protected fileApi_: any;
public constructor(db: any, options: any = null) {
this.db_ = db;
this.synchronizer_ = null;
this.initState_ = null;
this.logger_ = null;
this.options_ = options;
}
static supportsConfigCheck() {
public static supportsConfigCheck() {
return false;
}
option(name, defaultValue = null) {
public option(name: string, defaultValue: any = null) {
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
}
logger() {
protected logger() {
return this.logger_;
}
setLogger(v) {
public setLogger(v: Logger) {
this.logger_ = v;
}
db() {
protected db() {
return this.db_;
}
// If [] is returned it means all platforms are supported
static unsupportedPlatforms() {
public static unsupportedPlatforms(): any[] {
return [];
}
async isAuthenticated() {
public async isAuthenticated() {
return false;
}
authRouteName() {
public authRouteName(): string {
return null;
}
static id() {
public static id() {
throw new Error('id() not implemented');
}
// Note: it cannot be called just "name()" because that's a reserved keyword and
// it would throw an obscure error in React Native.
static targetName() {
public static targetName() {
throw new Error('targetName() not implemented');
}
static label() {
public static label() {
throw new Error('label() not implemented');
}
async initSynchronizer() {
protected async initSynchronizer(): Promise<Synchronizer> {
throw new Error('initSynchronizer() not implemented');
}
async initFileApi() {
protected async initFileApi(): Promise<any> {
throw new Error('initFileApi() not implemented');
}
async fileApi() {
public async fileApi() {
if (this.fileApi_) return this.fileApi_;
this.fileApi_ = await this.initFileApi();
return this.fileApi_;
}
fileApiSync() {
return this.fileApi_;
}
// Usually each sync target should create and setup its own file API via initFileApi()
// but for testing purposes it might be convenient to provide it here so that multiple
// clients can share and sync to the same file api (see test-utils.js)
setFileApi(v) {
public setFileApi(v: any) {
this.fileApi_ = v;
}
async synchronizer() {
public async synchronizer(): Promise<Synchronizer> {
if (this.synchronizer_) return this.synchronizer_;
if (this.initState_ == 'started') {
@@ -106,6 +112,7 @@ class BaseSyncTarget {
this.synchronizer_ = await this.initSynchronizer();
this.synchronizer_.setLogger(this.logger());
this.synchronizer_.setEncryptionService(EncryptionService.instance());
this.synchronizer_.setResourceService(ResourceService.instance());
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
this.initState_ = 'ready';
return this.synchronizer_;
@@ -116,14 +123,10 @@ class BaseSyncTarget {
}
}
async syncStarted() {
public async syncStarted() {
if (!this.synchronizer_) return false;
if (!(await this.isAuthenticated())) return false;
const sync = await this.synchronizer();
return sync.state() != 'idle';
}
}
BaseSyncTarget.dispatch = () => {};
module.exports = BaseSyncTarget;

View File

@@ -1,8 +1,9 @@
import Resource from './models/Resource';
import shim from './shim';
import Database, { SqlQuery } from './database';
const { promiseChain } = require('./promise-utils.js');
const { Database } = require('./database.js');
const { sprintf } = require('sprintf-js');
const Resource = require('./models/Resource').default;
const shim = require('./shim').default;
const structureSql = `
CREATE TABLE folders (
@@ -118,13 +119,28 @@ CREATE TABLE version (
INSERT INTO version (version) VALUES (1);
`;
class JoplinDatabase extends Database {
constructor(driver) {
interface TableField {
name: string;
type: number;
default: any;
description?: string;
}
export default class JoplinDatabase extends Database {
public static TYPE_INT = 1;
public static TYPE_TEXT = 2;
public static TYPE_NUMERIC = 3;
private initialized_ = false;
private tableFields_: Record<string, TableField[]> = null;
private version_: number = null;
private tableFieldNames_: Record<string, string[]> = {};
private tableDescriptions_: any;
constructor(driver: any) {
super(driver);
this.initialized_ = false;
this.tableFields_ = null;
this.version_ = null;
this.tableFieldNames_ = {};
// this.extensionToLoad = './build/lib/sql-extensions/spellfix';
}
@@ -132,12 +148,12 @@ class JoplinDatabase extends Database {
return this.initialized_;
}
async open(options) {
async open(options: any) {
await super.open(options);
return this.initialize();
}
tableFieldNames(tableName) {
tableFieldNames(tableName: string) {
if (this.tableFieldNames_[tableName]) return this.tableFieldNames_[tableName].slice();
const tf = this.tableFields(tableName);
@@ -150,7 +166,7 @@ class JoplinDatabase extends Database {
return output.slice();
}
tableFields(tableName, options = null) {
tableFields(tableName: string, options: any = null) {
if (options === null) options = {};
if (!this.tableFields_) throw new Error('Fields have not been loaded yet');
@@ -206,9 +222,9 @@ class JoplinDatabase extends Database {
await this.transactionExecBatch(queries);
}
createDefaultRow() {
const row = {};
const fields = this.tableFields('resource_local_states');
createDefaultRow(tableName: string) {
const row: any = {};
const fields = this.tableFields(tableName);
for (let i = 0; i < fields.length; i++) {
const f = fields[i];
row[f.name] = Database.formatValue(f.type, f.default);
@@ -216,7 +232,7 @@ class JoplinDatabase extends Database {
return row;
}
fieldByName(tableName, fieldName) {
fieldByName(tableName: string, fieldName: string) {
const fields = this.tableFields(tableName);
for (const field of fields) {
if (field.name === fieldName) return field;
@@ -224,11 +240,11 @@ class JoplinDatabase extends Database {
throw new Error(`No such field: ${tableName}: ${fieldName}`);
}
fieldDefaultValue(tableName, fieldName) {
fieldDefaultValue(tableName: string, fieldName: string) {
return this.fieldByName(tableName, fieldName).default;
}
fieldDescription(tableName, fieldName) {
fieldDescription(tableName: string, fieldName: string) {
const sp = sprintf;
if (!this.tableDescriptions_) {
@@ -264,9 +280,9 @@ class JoplinDatabase extends Database {
return d && d[fieldName] ? d[fieldName] : '';
}
refreshTableFields(newVersion) {
refreshTableFields(newVersion: number) {
this.logger().info('Initializing tables...');
const queries = [];
const queries: SqlQuery[] = [];
queries.push(this.wrapQuery('DELETE FROM table_fields'));
return this.selectAll('SELECT name FROM sqlite_master WHERE type="table"')
@@ -309,12 +325,12 @@ class JoplinDatabase extends Database {
});
}
addMigrationFile(num) {
addMigrationFile(num: number) {
const timestamp = Date.now();
return { sql: 'INSERT INTO migrations (number, created_time, updated_time) VALUES (?, ?, ?)', params: [num, timestamp, timestamp] };
}
async upgradeDatabase(fromVersion) {
async upgradeDatabase(fromVersion: number) {
// INSTRUCTIONS TO UPGRADE THE DATABASE:
//
// 1. Add the new version number to the existingDatabaseVersions array
@@ -353,7 +369,7 @@ class JoplinDatabase extends Database {
const targetVersion = existingDatabaseVersions[currentVersionIndex + 1];
this.logger().info(`Converting database to version ${targetVersion}`);
let queries = [];
let queries: any[] = [];
if (targetVersion == 1) {
queries = this.wrapQueries(this.sqlStringToLines(structureSql));
@@ -965,9 +981,3 @@ class JoplinDatabase extends Database {
}
}
}
Database.TYPE_INT = 1;
Database.TYPE_TEXT = 2;
Database.TYPE_NUMERIC = 3;
module.exports = { JoplinDatabase };

View File

@@ -1,163 +1,190 @@
import shim from './shim';
import { _ } from './locale';
import Logger from './Logger';
const { rtrimSlashes } = require('./path-utils.js');
const JoplinError = require('./JoplinError');
const { rtrimSlashes } = require('./path-utils');
const base64 = require('base-64');
const { stringify } = require('query-string');
interface JoplinServerApiOptions {
username: Function;
password: Function;
baseUrl: Function;
interface Options {
baseUrl(): string;
username(): string;
password(): string;
}
enum ExecOptionsResponseFormat {
Json = 'json',
Text = 'text',
}
enum ExecOptionsTarget {
String = 'string',
File = 'file',
}
interface ExecOptions {
responseFormat?: ExecOptionsResponseFormat;
target?: ExecOptionsTarget;
path?: string;
source?: string;
}
export default class JoplinServerApi {
logger_: any;
options_: JoplinServerApiOptions;
kvStore_: any;
private options_: Options;
private session_: any;
constructor(options: JoplinServerApiOptions) {
this.logger_ = new Logger();
public constructor(options: Options) {
this.options_ = options;
this.kvStore_ = null;
}
setLogger(l: any) {
this.logger_ = l;
}
logger(): any {
return this.logger_;
}
setKvStore(v: any) {
this.kvStore_ = v;
}
kvStore() {
if (!this.kvStore_) throw new Error('JoplinServerApi.kvStore_ is not set!!');
return this.kvStore_;
}
authToken(): string {
if (!this.options_.username() || !this.options_.password()) return null;
try {
// Note: Non-ASCII passwords will throw an error about Latin1 characters - https://github.com/laurent22/joplin/issues/246
// Tried various things like the below, but it didn't work on React Native:
// return base64.encode(utf8.encode(this.options_.username() + ':' + this.options_.password()));
return base64.encode(`${this.options_.username()}:${this.options_.password()}`);
} catch (error) {
error.message = `Cannot encode username/password: ${error.message}`;
throw error;
}
}
baseUrl(): string {
private baseUrl() {
return rtrimSlashes(this.options_.baseUrl());
}
static baseUrlFromNextcloudWebDavUrl(webDavUrl: string) {
// http://nextcloud.local/remote.php/webdav/Joplin
// http://nextcloud.local/index.php/apps/joplin/api
const splitted = webDavUrl.split('/remote.php/webdav');
if (splitted.length !== 2) throw new Error(`Unsupported WebDAV URL format: ${webDavUrl}`);
return `${splitted[0]}/index.php/apps/joplin/api`;
private async session() {
// TODO: handle invalid session
if (this.session_) return this.session_;
this.session_ = await this.exec('POST', 'api/sessions', null, {
email: this.options_.username(),
password: this.options_.password(),
});
return this.session_;
}
syncTargetId(settings: any) {
const s = settings['sync.5.syncTargets'][settings['sync.5.path']];
if (!s) throw new Error(`Joplin Nextcloud app not configured for URL: ${this.baseUrl()}`);
return s.uuid;
private async sessionId() {
const session = await this.session();
return session ? session.id : '';
}
static connectionErrorMessage(error: any) {
const msg = error && error.message ? error.message : 'Unknown error';
return _('Could not connect to the Joplin Nextcloud app. Please check the configuration in the Synchronisation config screen. Full error was:\n\n%s', msg);
}
async setupSyncTarget(webDavUrl: string) {
return this.exec('POST', 'sync_targets', {
webDavUrl: webDavUrl,
public async shareFile(pathOrId: string) {
return this.exec('POST', 'api/shares', null, {
file_id: pathOrId,
type: 1, // ShareType.Link
});
}
requestToCurl_(url: string, options: any) {
const output = [];
output.push('curl');
output.push('-v');
if (options.method) output.push(`-X ${options.method}`);
if (options.headers) {
for (const n in options.headers) {
if (!options.headers.hasOwnProperty(n)) continue;
output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
}
}
if (options.body) output.push(`${'--data ' + '\''}${options.body}'`);
output.push(url);
return output.join(' ');
public static connectionErrorMessage(error: any) {
const msg = error && error.message ? error.message : 'Unknown error';
return _('Could not connect to Joplin Server. Please check the Synchronisation options in the config screen. Full error was:\n\n%s', msg);
}
async exec(method: string, path: string = '', body: any = null, headers: any = null, options: any = null): Promise<any> {
public shareUrl(share: any): string {
return `${this.baseUrl()}/shares/${share.id}`;
}
// private requestToCurl_(url: string, options: any) {
// const output = [];
// output.push('curl');
// output.push('-v');
// if (options.method) output.push(`-X ${options.method}`);
// if (options.headers) {
// for (const n in options.headers) {
// if (!options.headers.hasOwnProperty(n)) continue;
// output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
// }
// }
// if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`);
// output.push(url);
// return output.join(' ');
// }
public async exec(method: string, path: string = '', query: Record<string, any> = null, body: any = null, headers: any = null, options: ExecOptions = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.responseFormat) options.responseFormat = ExecOptionsResponseFormat.Json;
if (!options.target) options.target = ExecOptionsTarget.String;
const authToken = this.authToken();
let sessionId = '';
if (path !== 'api/sessions' && !sessionId) {
sessionId = await this.sessionId();
}
if (authToken) headers['Authorization'] = `Basic ${authToken}`;
headers['Content-Type'] = 'application/json';
if (typeof body === 'object' && body !== null) body = JSON.stringify(body);
if (sessionId) headers['X-API-AUTH'] = sessionId;
const fetchOptions: any = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) fetchOptions.body = body;
const url = `${this.baseUrl()}/${path}`;
if (body) {
if (typeof body === 'object') {
fetchOptions.body = JSON.stringify(body);
fetchOptions.headers['Content-Type'] = 'application/json';
} else {
fetchOptions.body = body;
}
let response = null;
fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(fetchOptions.body)}`;
}
// console.info('WebDAV Call', method + ' ' + url, headers, options);
console.info(this.requestToCurl_(url, fetchOptions));
let url = `${this.baseUrl()}/${path}`;
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
response = await shim.fetch(url, fetchOptions);
if (query) {
url += url.indexOf('?') < 0 ? '?' : '&';
url += stringify(query);
}
let response: any = null;
// console.info('Joplin API Call', `${method} ${url}`, headers, options);
// console.info(this.requestToCurl_(url, fetchOptions));
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
if (fetchOptions.path) {
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
}
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
response = await shim.fetch(url, fetchOptions);
} else {
// file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
const responseJson_: any = null;
// console.info('Joplin API Response', responseText);
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
const newError = (message: string, code: number = 0) => {
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = (`${responseText}`).substr(0, 1024);
return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
};
let responseJson_: any = null;
const loadResponseJson = async () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
try {
return JSON.parse(responseText);
} catch (error) {
throw new Error(`Cannot parse JSON: ${responseText.substr(0, 8192)}`);
}
};
const newError = (message: string, code: number = 0) => {
return new JoplinError(`${method} ${path}: ${message} (${code})`, code);
responseJson_ = JSON.parse(responseText);
if (!responseJson_) throw newError('Cannot parse JSON response', response.status);
return responseJson_;
};
if (!response.ok) {
if (options.target === 'file') throw newError('fetchBlob error', response.status);
let json = null;
try {
json = await loadResponseJson();
} catch (error) {
throw newError(`Unknown error: ${responseText.substr(0, 8192)}`, response.status);
// Just send back the plain text in newErro()
}
const trace = json.stacktrace ? `\n${json.stacktrace}` : '';
let message = json.error;
if (!message) message = responseText.substr(0, 8192);
throw newError(message + trace, response.status);
if (json && json.error) {
throw newError(`${json.error}`, json.code ? json.code : response.status);
}
throw newError('Unknown error', response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
return output;
}

View File

@@ -1,174 +0,0 @@
import shim from './shim';
const { rtrimSlashes } = require('./path-utils.js');
const JoplinError = require('./JoplinError');
const { stringify } = require('query-string');
interface Options {
baseUrl(): string;
username(): string;
password(): string;
}
enum ExecOptionsResponseFormat {
Json = 'json',
Text = 'text',
}
enum ExecOptionsTarget {
String = 'string',
File = 'file',
}
interface ExecOptions {
responseFormat?: ExecOptionsResponseFormat;
target?: ExecOptionsTarget;
path?: string;
source?: string;
}
export default class JoplinServerApi {
private options_: Options;
private session_: any;
public constructor(options: Options) {
this.options_ = options;
}
private baseUrl() {
return rtrimSlashes(this.options_.baseUrl());
}
private async session() {
// TODO: handle invalid session
if (this.session_) return this.session_;
this.session_ = await this.exec('POST', 'api/sessions', null, {
email: this.options_.username(),
password: this.options_.password(),
});
return this.session_;
}
private async sessionId() {
const session = await this.session();
return session ? session.id : '';
}
// private requestToCurl_(url: string, options: any) {
// const output = [];
// output.push('curl');
// output.push('-v');
// if (options.method) output.push(`-X ${options.method}`);
// if (options.headers) {
// for (const n in options.headers) {
// if (!options.headers.hasOwnProperty(n)) continue;
// output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
// }
// }
// if (options.body) output.push(`${'--data ' + '\''}${JSON.stringify(options.body)}'`);
// output.push(url);
// return output.join(' ');
// }
public async exec(method: string, path: string = '', query: Record<string, any> = null, body: any = null, headers: any = null, options: ExecOptions = null) {
if (headers === null) headers = {};
if (options === null) options = {};
if (!options.responseFormat) options.responseFormat = ExecOptionsResponseFormat.Json;
if (!options.target) options.target = ExecOptionsTarget.String;
let sessionId = '';
if (path !== 'api/sessions' && !sessionId) {
sessionId = await this.sessionId();
}
if (sessionId) headers['X-API-AUTH'] = sessionId;
const fetchOptions: any = {};
fetchOptions.headers = headers;
fetchOptions.method = method;
if (options.path) fetchOptions.path = options.path;
if (body) {
if (typeof body === 'object') {
fetchOptions.body = JSON.stringify(body);
fetchOptions.headers['Content-Type'] = 'application/json';
} else {
fetchOptions.body = body;
}
fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(fetchOptions.body)}`;
}
let url = `${this.baseUrl()}/${path}`;
if (query) {
url += url.indexOf('?') < 0 ? '?' : '&';
url += stringify(query);
}
let response: any = null;
// console.info('Joplin API Call', `${method} ${url}`, headers, options);
// console.info(this.requestToCurl_(url, fetchOptions));
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
if (fetchOptions.path) {
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
}
response = await shim.uploadBlob(url, fetchOptions);
} else if (options.target == 'string') {
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
response = await shim.fetch(url, fetchOptions);
} else {
// file
response = await shim.fetchBlob(url, fetchOptions);
}
const responseText = await response.text();
// console.info('Joplin API Response', responseText);
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
const newError = (message: string, code: number = 0) => {
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
const shortResponseText = (`${responseText}`).substr(0, 1024);
return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
};
let responseJson_: any = null;
const loadResponseJson = async () => {
if (!responseText) return null;
if (responseJson_) return responseJson_;
responseJson_ = JSON.parse(responseText);
if (!responseJson_) throw newError('Cannot parse JSON response', response.status);
return responseJson_;
};
if (!response.ok) {
if (options.target === 'file') throw newError('fetchBlob error', response.status);
let json = null;
try {
json = await loadResponseJson();
} catch (error) {
// Just send back the plain text in newErro()
}
if (json && json.message) {
throw newError(`${json.message}`, response.status);
}
throw newError('Unknown error', response.status);
}
if (options.responseFormat === 'text') return responseText;
const output = await loadResponseJson();
return output;
}
}

View File

@@ -1,4 +1,4 @@
const BaseSyncTarget = require('./BaseSyncTarget.js');
const BaseSyncTarget = require('./BaseSyncTarget').default;
const { _ } = require('./locale');
const Setting = require('./models/Setting').default;
const { FileApi } = require('./file-api.js');

View File

@@ -1,4 +1,4 @@
const BaseSyncTarget = require('./BaseSyncTarget.js');
const BaseSyncTarget = require('./BaseSyncTarget').default;
const { _ } = require('./locale');
const DropboxApi = require('./DropboxApi');
const Setting = require('./models/Setting').default;

View File

@@ -1,4 +1,4 @@
const BaseSyncTarget = require('./BaseSyncTarget.js');
const BaseSyncTarget = require('./BaseSyncTarget').default;
const { _ } = require('./locale');
const Setting = require('./models/Setting').default;
const { FileApi } = require('./file-api.js');

View File

@@ -2,10 +2,9 @@ import FileApiDriverJoplinServer from './file-api-driver-joplinServer';
import Setting from './models/Setting';
import Synchronizer from './Synchronizer';
import { _ } from './locale.js';
import JoplinServerApi from './JoplinServerApi2';
const BaseSyncTarget = require('./BaseSyncTarget.js');
const { FileApi } = require('./file-api.js');
import JoplinServerApi from './JoplinServerApi';
import BaseSyncTarget from './BaseSyncTarget';
import { FileApi } from './file-api';
interface FileApiOptions {
path(): string;
@@ -16,27 +15,31 @@ interface FileApiOptions {
export default class SyncTargetJoplinServer extends BaseSyncTarget {
static id() {
public static id() {
return 9;
}
static supportsConfigCheck() {
public static supportsConfigCheck() {
return true;
}
static targetName() {
public static targetName() {
return 'joplinServer';
}
static label() {
public static label() {
return _('Joplin Server');
}
async isAuthenticated() {
public async isAuthenticated() {
return true;
}
static async newFileApi_(options: FileApiOptions) {
public async fileApi(): Promise<FileApi> {
return super.fileApi();
}
private static async newFileApi_(options: FileApiOptions) {
const apiOptions = {
baseUrl: () => options.path(),
username: () => options.username(),
@@ -51,7 +54,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
return fileApi;
}
static async checkConfig(options: FileApiOptions) {
public static async checkConfig(options: FileApiOptions) {
const output = {
ok: false,
errorMessage: '',
@@ -72,7 +75,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
return output;
}
async initFileApi() {
protected async initFileApi() {
const fileApi = await SyncTargetJoplinServer.newFileApi_({
path: () => Setting.value('sync.9.path'),
username: () => Setting.value('sync.9.username'),
@@ -85,7 +88,7 @@ export default class SyncTargetJoplinServer extends BaseSyncTarget {
return fileApi;
}
async initSynchronizer() {
protected async initSynchronizer() {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
}

View File

@@ -1,4 +1,4 @@
const BaseSyncTarget = require('./BaseSyncTarget.js');
const BaseSyncTarget = require('./BaseSyncTarget').default;
const Setting = require('./models/Setting').default;
const { FileApi } = require('./file-api.js');
const { FileApiDriverMemory } = require('./file-api-driver-memory.js');

View File

@@ -1,12 +1,11 @@
// The Nextcloud sync target is essentially a wrapper over the WebDAV sync target,
// thus all the calls to SyncTargetWebDAV to avoid duplicate code.
const BaseSyncTarget = require('./BaseSyncTarget.js');
const BaseSyncTarget = require('./BaseSyncTarget').default;
const { _ } = require('./locale');
const Setting = require('./models/Setting').default;
const Synchronizer = require('./Synchronizer').default;
const SyncTargetWebDAV = require('./SyncTargetWebDAV');
const JoplinServerApi = require('./JoplinServerApi.js').default;
class SyncTargetNextcloud extends BaseSyncTarget {
@@ -50,24 +49,6 @@ class SyncTargetNextcloud extends BaseSyncTarget {
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
}
async appApi(settings = null) {
const useCache = !settings;
if (this.appApi_ && useCache) return this.appApi_;
const appApi = new JoplinServerApi({
baseUrl: () => JoplinServerApi.baseUrlFromNextcloudWebDavUrl(settings ? settings['sync.5.path'] : Setting.value('sync.5.path')),
username: () => settings ? settings['sync.5.username'] : Setting.value('sync.5.username'),
password: () => settings ? settings['sync.5.password'] : Setting.value('sync.5.password'),
});
appApi.setLogger(this.logger());
if (useCache) this.appApi_ = appApi;
return appApi;
}
}
module.exports = SyncTargetNextcloud;

View File

@@ -2,14 +2,16 @@ import OneDriveApi from './onedrive-api';
import { _ } from './locale';
import Setting from './models/Setting';
import Synchronizer from './Synchronizer';
import BaseSyncTarget from './BaseSyncTarget';
const BaseSyncTarget = require('./BaseSyncTarget.js');
const { parameters } = require('./parameters.js');
const { FileApi } = require('./file-api.js');
const { FileApiDriverOneDrive } = require('./file-api-driver-onedrive.js');
export default class SyncTargetOneDrive extends BaseSyncTarget {
private api_: any;
static id() {
return 3;
}

View File

@@ -1,4 +1,4 @@
const BaseSyncTarget = require('./BaseSyncTarget.js');
const BaseSyncTarget = require('./BaseSyncTarget').default;
const { _ } = require('./locale');
const Setting = require('./models/Setting').default;
const { FileApi } = require('./file-api.js');

View File

@@ -16,6 +16,9 @@ import MasterKey from './models/MasterKey';
import BaseModel from './BaseModel';
const { sprintf } = require('sprintf-js');
import time from './time';
import ResourceService from './services/ResourceService';
import EncryptionService from './services/EncryptionService';
import NoteResource from './models/NoteResource';
const JoplinError = require('./JoplinError');
const TaskQueue = require('./TaskQueue');
const { Dirnames } = require('./services/synchronizer/utils/types');
@@ -39,7 +42,8 @@ export default class Synchronizer {
private clientId_: string;
private lockHandler_: LockHandler;
private migrationHandler_: MigrationHandler;
private encryptionService_: any = null;
private encryptionService_: EncryptionService = null;
private resourceService_: ResourceService = null;
private syncTargetIsLocked_: boolean = false;
// Debug flags are used to test certain hard-to-test conditions
@@ -104,7 +108,7 @@ export default class Synchronizer {
return this.appType_ === 'mobile' ? 100 * 1000 * 1000 : Infinity;
}
setEncryptionService(v: any) {
public setEncryptionService(v: any) {
this.encryptionService_ = v;
}
@@ -112,6 +116,14 @@ export default class Synchronizer {
return this.encryptionService_;
}
public setResourceService(v: ResourceService) {
this.resourceService_ = v;
}
protected resourceService(): ResourceService {
return this.resourceService_;
}
async waitForSyncToFinish() {
if (this.state() === 'idle') return;
@@ -220,7 +232,7 @@ export default class Synchronizer {
const iid = shim.setInterval(() => {
if (this.state() == 'idle') {
shim.clearInterval(iid);
resolve();
resolve(null);
}
}, 100);
});
@@ -332,6 +344,19 @@ export default class Synchronizer {
return `${Dirnames.Resources}/${resourceId}`;
};
// We index resources and apply the "is_shared" flag before syncing
// because it's going to affect what's sent encrypted, and what's sent
// plain text.
try {
if (this.resourceService()) {
this.logger().info('Indexing resources...');
await this.resourceService().indexNoteResources();
await NoteResource.applySharedStatusToLinkedResources();
}
} catch (error) {
this.logger().error('Error indexing resources:', error);
}
let errorToThrow = null;
let syncLock = null;

View File

@@ -1,6 +1,6 @@
import { utils, CommandRuntime, CommandDeclaration, CommandContext } from '../services/CommandService';
import { _ } from '../locale';
const { reg } = require('../registry.js');
import { reg } from '../registry';
export const declaration: CommandDeclaration = {
name: 'synchronize',
@@ -43,7 +43,7 @@ export const runtime = (): CommandRuntime => {
sync.cancel();
return 'cancel';
} else {
reg.scheduleSync(0);
void reg.scheduleSync(0);
return 'sync';
}
},

View File

@@ -3,7 +3,6 @@ const SyncTargetRegistry = require('../../SyncTargetRegistry');
const ObjectUtils = require('../../ObjectUtils');
const { _ } = require('../../locale');
const { createSelector } = require('reselect');
const { reg } = require('../../registry');
const shared = {};
@@ -32,7 +31,7 @@ shared.checkSyncConfig = async function(comp, settings) {
comp.setState({ checkSyncConfigResult: result });
if (result.ok) {
await shared.checkNextcloudApp(comp, settings);
// await shared.checkNextcloudApp(comp, settings);
// Users often expect config to be auto-saved at this point, if the config check was successful
shared.saveSettings(comp);
}
@@ -54,30 +53,6 @@ shared.checkSyncConfigMessages = function(comp) {
return output;
};
shared.checkNextcloudApp = async function(comp, settings) {
if (settings['sync.target'] !== 5) return;
comp.setState({ checkNextcloudAppResult: 'checking' });
let result = null;
const appApi = await reg.syncTargetNextcloud().appApi(settings);
try {
result = await appApi.setupSyncTarget(settings['sync.5.path']);
} catch (error) {
reg.logger().error('Could not setup sync target:', error);
result = { error: error.message };
}
const newSyncTargets = Object.assign({}, settings['sync.5.syncTargets']);
newSyncTargets[settings['sync.5.path']] = result;
shared.updateSettingValue(comp, 'sync.5.syncTargets', newSyncTargets);
// Also immediately save the result as this is most likely what the user would expect
Setting.setValue('sync.5.syncTargets', newSyncTargets);
comp.setState({ checkNextcloudAppResult: 'done' });
};
shared.updateSettingValue = function(comp, key, value) {
comp.setState(state => {
const settings = Object.assign({}, state.settings);

View File

@@ -1,321 +1,358 @@
const Logger = require('./Logger').default;
const time = require('./time').default;
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const Logger_1 = require("./Logger");
const time_1 = require("./time");
const shim_1 = require("./shim");
const Mutex = require('async-mutex').Mutex;
const shim = require('./shim').default;
class Database {
constructor(driver) {
this.debugMode_ = false;
this.sqlQueryLogEnabled_ = false;
this.driver_ = driver;
this.logger_ = new Logger();
this.logExcludedQueryTypes_ = [];
this.batchTransactionMutex_ = new Mutex();
this.profilingEnabled_ = false;
this.queryId_ = 1;
}
setLogExcludedQueryTypes(v) {
this.logExcludedQueryTypes_ = v;
}
// Converts the SQLite error to a regular JS error
// so that it prints a stacktrace when passed to
// console.error()
sqliteErrorToJsError(error, sql = null, params = null) {
return this.driver().sqliteErrorToJsError(error, sql, params);
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
driver() {
return this.driver_;
}
async open(options) {
try {
await this.driver().open(options);
} catch (error) {
throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`);
}
this.logger().info('Database was open successfully');
}
escapeField(field) {
if (field == '*') return '*';
const p = field.split('.');
if (p.length == 1) return `\`${field}\``;
if (p.length == 2) return `${p[0]}.\`${p[1]}\``;
throw new Error(`Invalid field format: ${field}`);
}
escapeFields(fields) {
if (fields == '*') return '*';
const output = [];
for (let i = 0; i < fields.length; i++) {
output.push(this.escapeField(fields[i]));
}
return output;
}
async tryCall(callName, sql, params) {
if (typeof sql === 'object') {
params = sql.params;
sql = sql.sql;
}
let waitTime = 50;
let totalWaitTime = 0;
const callStartTime = Date.now();
let profilingTimeoutId = null;
while (true) {
try {
this.logQuery(sql, params);
const queryId = this.queryId_++;
if (this.profilingEnabled_) {
console.info(`SQL START ${queryId}`, sql, params);
profilingTimeoutId = shim.setInterval(() => {
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
}, 3000);
}
const result = await this.driver()[callName](sql, params);
if (this.profilingEnabled_) {
shim.clearInterval(profilingTimeoutId);
profilingTimeoutId = null;
const elapsed = Date.now() - callStartTime;
if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params);
}
return result; // No exception was thrown
} catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
// NOTE: don't put logger statements here because it might log to the database, which
// could result in an error being thrown again.
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
// this.logger().warn('Error was: ' + error.toString());
await time.msleep(waitTime);
totalWaitTime += waitTime;
waitTime *= 1.5;
} else {
throw this.sqliteErrorToJsError(error, sql, params);
}
} finally {
if (profilingTimeoutId) shim.clearInterval(profilingTimeoutId);
}
}
}
async selectOne(sql, params = null) {
return this.tryCall('selectOne', sql, params);
}
async loadExtension(/* path */) {
return; // Disabled for now as fuzzy search extension is not in use
// let result = null;
// try {
// result = await this.driver().loadExtension(path);
// return result;
// } catch (e) {
// throw new Error(`Could not load extension ${path}`);
// }
}
async selectAll(sql, params = null) {
return this.tryCall('selectAll', sql, params);
}
async selectAllFields(sql, params, field) {
const rows = await this.tryCall('selectAll', sql, params);
const output = [];
for (let i = 0; i < rows.length; i++) {
const v = rows[i][field];
if (!v) throw new Error(`No such field: ${field}. Query was: ${sql}`);
output.push(rows[i][field]);
}
return output;
}
async exec(sql, params = null) {
return this.tryCall('exec', sql, params);
}
async transactionExecBatch(queries) {
if (queries.length <= 0) return;
if (queries.length == 1) {
const q = this.wrapQuery(queries[0]);
await this.exec(q.sql, q.params);
return;
}
// There can be only one transaction running at a time so use a mutex
const release = await this.batchTransactionMutex_.acquire();
try {
await this.exec('BEGIN TRANSACTION');
for (let i = 0; i < queries.length; i++) {
const query = this.wrapQuery(queries[i]);
await this.exec(query.sql, query.params);
}
await this.exec('COMMIT');
} catch (error) {
await this.exec('ROLLBACK');
throw error;
} finally {
release();
}
}
static enumId(type, s) {
if (type == 'settings') {
if (s == 'int') return 1;
if (s == 'string') return 2;
}
if (type == 'fieldType') {
if (s) s = s.toUpperCase();
if (s == 'INTEGER') s = 'INT';
if (!(`TYPE_${s}` in this)) throw new Error(`Unkonwn fieldType: ${s}`);
return this[`TYPE_${s}`];
}
if (type == 'syncTarget') {
if (s == 'memory') return 1;
if (s == 'filesystem') return 2;
if (s == 'onedrive') return 3;
}
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
}
static enumName(type, id) {
if (type === 'fieldType') {
if (id === Database.TYPE_UNKNOWN) return 'unknown';
if (id === Database.TYPE_INT) return 'int';
if (id === Database.TYPE_TEXT) return 'text';
if (id === Database.TYPE_NUMERIC) return 'numeric';
throw new Error(`Invalid type id: ${id}`);
}
}
static formatValue(type, value) {
if (value === null || value === undefined) return null;
if (type == this.TYPE_INT) return Number(value);
if (type == this.TYPE_TEXT) return value;
if (type == this.TYPE_NUMERIC) return Number(value);
throw new Error(`Unknown type: ${type}`);
}
sqlStringToLines(sql) {
const output = [];
const lines = sql.split('\n');
let statement = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line == '') continue;
if (line.substr(0, 2) == '--') continue;
statement += line.trim();
if (line[line.length - 1] == ',') statement += ' ';
if (line[line.length - 1] == ';') {
output.push(statement);
statement = '';
}
}
return output;
}
logQuery(sql, params = null) {
if (!this.sqlQueryLogEnabled_) return;
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return;
}
}
this.logger().debug(sql);
if (params !== null && params.length) this.logger().debug(JSON.stringify(params));
}
static insertQuery(tableName, data) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
let keySql = '';
let valueSql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key[key.length - 1] == '_') continue;
if (keySql != '') keySql += ', ';
if (valueSql != '') valueSql += ', ';
keySql += `\`${key}\``;
valueSql += '?';
params.push(data[key]);
}
return {
sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`,
params: params,
};
}
static updateQuery(tableName, data, where) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
let sql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key[key.length - 1] == '_') continue;
if (sql != '') sql += ', ';
sql += `\`${key}\`=?`;
params.push(data[key]);
}
if (typeof where != 'string') {
const s = [];
for (const n in where) {
if (!where.hasOwnProperty(n)) continue;
params.push(where[n]);
s.push(`\`${n}\`=?`);
}
where = s.join(' AND ');
}
return {
sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`,
params: params,
};
}
alterColumnQueries(tableName, fields) {
const fieldsNoType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n)) continue;
fieldsNoType.push(n);
}
const fieldsWithType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n)) continue;
fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`);
}
let sql = `
constructor(driver) {
this.debugMode_ = false;
this.sqlQueryLogEnabled_ = false;
this.logger_ = new Logger_1.default();
this.logExcludedQueryTypes_ = [];
this.batchTransactionMutex_ = new Mutex();
this.profilingEnabled_ = false;
this.queryId_ = 1;
this.driver_ = driver;
}
setLogExcludedQueryTypes(v) {
this.logExcludedQueryTypes_ = v;
}
// Converts the SQLite error to a regular JS error
// so that it prints a stacktrace when passed to
// console.error()
sqliteErrorToJsError(error, sql = null, params = null) {
return this.driver().sqliteErrorToJsError(error, sql, params);
}
setLogger(l) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
driver() {
return this.driver_;
}
open(options) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield this.driver().open(options);
}
catch (error) {
throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`);
}
this.logger().info('Database was open successfully');
});
}
escapeField(field) {
if (field == '*')
return '*';
const p = field.split('.');
if (p.length == 1)
return `\`${field}\``;
if (p.length == 2)
return `${p[0]}.\`${p[1]}\``;
throw new Error(`Invalid field format: ${field}`);
}
escapeFields(fields) {
if (fields == '*')
return '*';
const output = [];
for (let i = 0; i < fields.length; i++) {
output.push(this.escapeField(fields[i]));
}
return output;
}
tryCall(callName, inputSql, inputParams) {
return __awaiter(this, void 0, void 0, function* () {
let sql = null;
let params = null;
if (typeof inputSql === 'object') {
params = inputSql.params;
sql = inputSql.sql;
}
else {
params = inputParams;
sql = inputSql;
}
let waitTime = 50;
let totalWaitTime = 0;
const callStartTime = Date.now();
let profilingTimeoutId = null;
while (true) {
try {
this.logQuery(sql, params);
const queryId = this.queryId_++;
if (this.profilingEnabled_) {
console.info(`SQL START ${queryId}`, sql, params);
profilingTimeoutId = shim_1.default.setInterval(() => {
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
}, 3000);
}
const result = yield this.driver()[callName](sql, params);
if (this.profilingEnabled_) {
shim_1.default.clearInterval(profilingTimeoutId);
profilingTimeoutId = null;
const elapsed = Date.now() - callStartTime;
if (elapsed > 10)
console.info(`SQL END ${queryId}`, elapsed, sql, params);
}
return result; // No exception was thrown
}
catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
if (totalWaitTime >= 20000)
throw this.sqliteErrorToJsError(error, sql, params);
// NOTE: don't put logger statements here because it might log to the database, which
// could result in an error being thrown again.
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
// this.logger().warn('Error was: ' + error.toString());
yield time_1.default.msleep(waitTime);
totalWaitTime += waitTime;
waitTime *= 1.5;
}
else {
throw this.sqliteErrorToJsError(error, sql, params);
}
}
finally {
if (profilingTimeoutId)
shim_1.default.clearInterval(profilingTimeoutId);
}
}
});
}
selectOne(sql, params = null) {
return __awaiter(this, void 0, void 0, function* () {
return this.tryCall('selectOne', sql, params);
});
}
loadExtension( /* path */) {
return __awaiter(this, void 0, void 0, function* () {
return; // Disabled for now as fuzzy search extension is not in use
// let result = null;
// try {
// result = await this.driver().loadExtension(path);
// return result;
// } catch (e) {
// throw new Error(`Could not load extension ${path}`);
// }
});
}
selectAll(sql, params = null) {
return __awaiter(this, void 0, void 0, function* () {
return this.tryCall('selectAll', sql, params);
});
}
selectAllFields(sql, params, field) {
return __awaiter(this, void 0, void 0, function* () {
const rows = yield this.tryCall('selectAll', sql, params);
const output = [];
for (let i = 0; i < rows.length; i++) {
const v = rows[i][field];
if (!v)
throw new Error(`No such field: ${field}. Query was: ${sql}`);
output.push(rows[i][field]);
}
return output;
});
}
exec(sql, params = null) {
return __awaiter(this, void 0, void 0, function* () {
return this.tryCall('exec', sql, params);
});
}
transactionExecBatch(queries) {
return __awaiter(this, void 0, void 0, function* () {
if (queries.length <= 0)
return;
if (queries.length == 1) {
const q = this.wrapQuery(queries[0]);
yield this.exec(q.sql, q.params);
return;
}
// There can be only one transaction running at a time so use a mutex
const release = yield this.batchTransactionMutex_.acquire();
try {
yield this.exec('BEGIN TRANSACTION');
for (let i = 0; i < queries.length; i++) {
const query = this.wrapQuery(queries[i]);
yield this.exec(query.sql, query.params);
}
yield this.exec('COMMIT');
}
catch (error) {
yield this.exec('ROLLBACK');
throw error;
}
finally {
release();
}
});
}
static enumId(type, s) {
if (type == 'settings') {
if (s == 'int')
return 1;
if (s == 'string')
return 2;
}
if (type == 'fieldType') {
if (s)
s = s.toUpperCase();
if (s == 'INTEGER')
s = 'INT';
if (!(`TYPE_${s}` in this))
throw new Error(`Unkonwn fieldType: ${s}`);
return this[`TYPE_${s}`];
}
if (type == 'syncTarget') {
if (s == 'memory')
return 1;
if (s == 'filesystem')
return 2;
if (s == 'onedrive')
return 3;
}
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
}
static enumName(type, id) {
if (type === 'fieldType') {
if (id === Database.TYPE_UNKNOWN)
return 'unknown';
if (id === Database.TYPE_INT)
return 'int';
if (id === Database.TYPE_TEXT)
return 'text';
if (id === Database.TYPE_NUMERIC)
return 'numeric';
throw new Error(`Invalid type id: ${id}`);
}
// Or maybe an error should be thrown
return undefined;
}
static formatValue(type, value) {
if (value === null || value === undefined)
return null;
if (type == this.TYPE_INT)
return Number(value);
if (type == this.TYPE_TEXT)
return value;
if (type == this.TYPE_NUMERIC)
return Number(value);
throw new Error(`Unknown type: ${type}`);
}
sqlStringToLines(sql) {
const output = [];
const lines = sql.split('\n');
let statement = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line == '')
continue;
if (line.substr(0, 2) == '--')
continue;
statement += line.trim();
if (line[line.length - 1] == ',')
statement += ' ';
if (line[line.length - 1] == ';') {
output.push(statement);
statement = '';
}
}
return output;
}
logQuery(sql, params = null) {
if (!this.sqlQueryLogEnabled_)
return;
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0)
return;
}
}
this.logger().debug(sql);
if (params !== null && params.length)
this.logger().debug(JSON.stringify(params));
}
static insertQuery(tableName, data) {
if (!data || !Object.keys(data).length)
throw new Error('Data is empty');
let keySql = '';
let valueSql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key))
continue;
if (key[key.length - 1] == '_')
continue;
if (keySql != '')
keySql += ', ';
if (valueSql != '')
valueSql += ', ';
keySql += `\`${key}\``;
valueSql += '?';
params.push(data[key]);
}
return {
sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`,
params: params,
};
}
static updateQuery(tableName, data, where) {
if (!data || !Object.keys(data).length)
throw new Error('Data is empty');
let sql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key))
continue;
if (key[key.length - 1] == '_')
continue;
if (sql != '')
sql += ', ';
sql += `\`${key}\`=?`;
params.push(data[key]);
}
if (typeof where != 'string') {
const s = [];
for (const n in where) {
if (!where.hasOwnProperty(n))
continue;
params.push(where[n]);
s.push(`\`${n}\`=?`);
}
where = s.join(' AND ');
}
return {
sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`,
params: params,
};
}
alterColumnQueries(tableName, fields) {
const fieldsNoType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n))
continue;
fieldsNoType.push(n);
}
const fieldsWithType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n))
continue;
fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`);
}
let sql = `
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_;
DROP TABLE _TABLE_NAME_;
@@ -323,42 +360,39 @@ class Database {
INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_;
DROP TABLE _BACKUP_TABLE_NAME_;
`;
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`));
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(','));
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
return sql.trim().split('\n');
}
wrapQueries(queries) {
const output = [];
for (let i = 0; i < queries.length; i++) {
output.push(this.wrapQuery(queries[i]));
}
return output;
}
wrapQuery(sql, params = null) {
if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`);
if (sql.constructor === Array) {
const output = {};
output.sql = sql[0];
output.params = sql.length >= 2 ? sql[1] : null;
return output;
} else if (typeof sql === 'string') {
return { sql: sql, params: params };
} else {
return sql; // Already wrapped
}
}
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`));
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(','));
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
return sql.trim().split('\n');
}
wrapQueries(queries) {
const output = [];
for (let i = 0; i < queries.length; i++) {
output.push(this.wrapQuery(queries[i]));
}
return output;
}
wrapQuery(sql, params = null) {
if (!sql)
throw new Error(`Cannot wrap empty string: ${sql}`);
if (Array.isArray(sql)) {
return {
sql: sql[0],
params: sql.length >= 2 ? sql[1] : null,
};
}
else if (typeof sql === 'string') {
return { sql: sql, params: params };
}
else {
return sql; // Already wrapped
}
}
}
exports.default = Database;
Database.TYPE_UNKNOWN = 0;
Database.TYPE_INT = 1;
Database.TYPE_TEXT = 2;
Database.TYPE_NUMERIC = 3;
module.exports = { Database };
//# sourceMappingURL=database.js.map

386
packages/lib/database.ts Normal file
View File

@@ -0,0 +1,386 @@
import Logger from './Logger';
import time from './time';
import shim from './shim';
const Mutex = require('async-mutex').Mutex;
type SqlParams = Record<string, any>;
export interface SqlQuery {
sql: string;
params?: SqlParams;
}
type StringOrSqlQuery = string | SqlQuery;
export type Row = Record<string, any>;
export default class Database {
public static TYPE_UNKNOWN = 0;
public static TYPE_INT = 1;
public static TYPE_TEXT = 2;
public static TYPE_NUMERIC = 3;
protected debugMode_ = false;
private sqlQueryLogEnabled_ = false;
private driver_: any;
private logger_ = new Logger();
private logExcludedQueryTypes_: string[] = [];
private batchTransactionMutex_ = new Mutex();
private profilingEnabled_ = false;
private queryId_ = 1;
public constructor(driver: any) {
this.driver_ = driver;
}
setLogExcludedQueryTypes(v: string[]) {
this.logExcludedQueryTypes_ = v;
}
// Converts the SQLite error to a regular JS error
// so that it prints a stacktrace when passed to
// console.error()
sqliteErrorToJsError(error: any, sql: string = null, params: SqlParams = null) {
return this.driver().sqliteErrorToJsError(error, sql, params);
}
setLogger(l: Logger) {
this.logger_ = l;
}
logger() {
return this.logger_;
}
driver() {
return this.driver_;
}
async open(options: any) {
try {
await this.driver().open(options);
} catch (error) {
throw new Error(`Cannot open database: ${error.message}: ${JSON.stringify(options)}`);
}
this.logger().info('Database was open successfully');
}
escapeField(field: string) {
if (field == '*') return '*';
const p = field.split('.');
if (p.length == 1) return `\`${field}\``;
if (p.length == 2) return `${p[0]}.\`${p[1]}\``;
throw new Error(`Invalid field format: ${field}`);
}
escapeFields(fields: string[] | string): string[] | string {
if (fields == '*') return '*';
const output = [];
for (let i = 0; i < fields.length; i++) {
output.push(this.escapeField(fields[i]));
}
return output;
}
async tryCall(callName: string, inputSql: StringOrSqlQuery, inputParams: SqlParams) {
let sql: string = null;
let params: SqlParams = null;
if (typeof inputSql === 'object') {
params = (inputSql as SqlQuery).params;
sql = (inputSql as SqlQuery).sql;
} else {
params = inputParams;
sql = inputSql as string;
}
let waitTime = 50;
let totalWaitTime = 0;
const callStartTime = Date.now();
let profilingTimeoutId = null;
while (true) {
try {
this.logQuery(sql, params);
const queryId = this.queryId_++;
if (this.profilingEnabled_) {
console.info(`SQL START ${queryId}`, sql, params);
profilingTimeoutId = shim.setInterval(() => {
console.warn(`SQL ${queryId} has been running for ${Date.now() - callStartTime}: ${sql}`);
}, 3000);
}
const result = await this.driver()[callName](sql, params);
if (this.profilingEnabled_) {
shim.clearInterval(profilingTimeoutId);
profilingTimeoutId = null;
const elapsed = Date.now() - callStartTime;
if (elapsed > 10) console.info(`SQL END ${queryId}`, elapsed, sql, params);
}
return result; // No exception was thrown
} catch (error) {
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
// NOTE: don't put logger statements here because it might log to the database, which
// could result in an error being thrown again.
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
// this.logger().warn('Error was: ' + error.toString());
await time.msleep(waitTime);
totalWaitTime += waitTime;
waitTime *= 1.5;
} else {
throw this.sqliteErrorToJsError(error, sql, params);
}
} finally {
if (profilingTimeoutId) shim.clearInterval(profilingTimeoutId);
}
}
}
async selectOne(sql: string, params: SqlParams = null): Promise<Row> {
return this.tryCall('selectOne', sql, params);
}
async loadExtension(/* path */) {
return; // Disabled for now as fuzzy search extension is not in use
// let result = null;
// try {
// result = await this.driver().loadExtension(path);
// return result;
// } catch (e) {
// throw new Error(`Could not load extension ${path}`);
// }
}
async selectAll(sql: string, params: SqlParams = null): Promise<Row[]> {
return this.tryCall('selectAll', sql, params);
}
async selectAllFields(sql: string, params: SqlParams, field: string): Promise<any[]> {
const rows = await this.tryCall('selectAll', sql, params);
const output = [];
for (let i = 0; i < rows.length; i++) {
const v = rows[i][field];
if (!v) throw new Error(`No such field: ${field}. Query was: ${sql}`);
output.push(rows[i][field]);
}
return output;
}
async exec(sql: StringOrSqlQuery, params: SqlParams = null) {
return this.tryCall('exec', sql, params);
}
async transactionExecBatch(queries: StringOrSqlQuery[]) {
if (queries.length <= 0) return;
if (queries.length == 1) {
const q = this.wrapQuery(queries[0]);
await this.exec(q.sql, q.params);
return;
}
// There can be only one transaction running at a time so use a mutex
const release = await this.batchTransactionMutex_.acquire();
try {
await this.exec('BEGIN TRANSACTION');
for (let i = 0; i < queries.length; i++) {
const query = this.wrapQuery(queries[i]);
await this.exec(query.sql, query.params);
}
await this.exec('COMMIT');
} catch (error) {
await this.exec('ROLLBACK');
throw error;
} finally {
release();
}
}
static enumId(type: string, s: string) {
if (type == 'settings') {
if (s == 'int') return 1;
if (s == 'string') return 2;
}
if (type == 'fieldType') {
if (s) s = s.toUpperCase();
if (s == 'INTEGER') s = 'INT';
if (!(`TYPE_${s}` in this)) throw new Error(`Unkonwn fieldType: ${s}`);
return (this as any)[`TYPE_${s}`];
}
if (type == 'syncTarget') {
if (s == 'memory') return 1;
if (s == 'filesystem') return 2;
if (s == 'onedrive') return 3;
}
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
}
static enumName(type: string, id: number) {
if (type === 'fieldType') {
if (id === Database.TYPE_UNKNOWN) return 'unknown';
if (id === Database.TYPE_INT) return 'int';
if (id === Database.TYPE_TEXT) return 'text';
if (id === Database.TYPE_NUMERIC) return 'numeric';
throw new Error(`Invalid type id: ${id}`);
}
// Or maybe an error should be thrown
return undefined;
}
static formatValue(type: number, value: any) {
if (value === null || value === undefined) return null;
if (type == this.TYPE_INT) return Number(value);
if (type == this.TYPE_TEXT) return value;
if (type == this.TYPE_NUMERIC) return Number(value);
throw new Error(`Unknown type: ${type}`);
}
sqlStringToLines(sql: string) {
const output = [];
const lines = sql.split('\n');
let statement = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line == '') continue;
if (line.substr(0, 2) == '--') continue;
statement += line.trim();
if (line[line.length - 1] == ',') statement += ' ';
if (line[line.length - 1] == ';') {
output.push(statement);
statement = '';
}
}
return output;
}
logQuery(sql: string, params: SqlParams = null) {
if (!this.sqlQueryLogEnabled_) return;
if (this.logExcludedQueryTypes_.length) {
const temp = sql.toLowerCase();
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return;
}
}
this.logger().debug(sql);
if (params !== null && params.length) this.logger().debug(JSON.stringify(params));
}
static insertQuery(tableName: string, data: Record<string, any>) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
let keySql = '';
let valueSql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key[key.length - 1] == '_') continue;
if (keySql != '') keySql += ', ';
if (valueSql != '') valueSql += ', ';
keySql += `\`${key}\``;
valueSql += '?';
params.push(data[key]);
}
return {
sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`,
params: params,
};
}
static updateQuery(tableName: string, data: Record<string, any>, where: string | Record<string, any>) {
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
let sql = '';
const params = [];
for (const key in data) {
if (!data.hasOwnProperty(key)) continue;
if (key[key.length - 1] == '_') continue;
if (sql != '') sql += ', ';
sql += `\`${key}\`=?`;
params.push(data[key]);
}
if (typeof where != 'string') {
const s = [];
for (const n in where) {
if (!where.hasOwnProperty(n)) continue;
params.push(where[n]);
s.push(`\`${n}\`=?`);
}
where = s.join(' AND ');
}
return {
sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`,
params: params,
};
}
alterColumnQueries(tableName: string, fields: Record<string, string>) {
const fieldsNoType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n)) continue;
fieldsNoType.push(n);
}
const fieldsWithType = [];
for (const n in fields) {
if (!fields.hasOwnProperty(n)) continue;
fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`);
}
let sql = `
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_;
DROP TABLE _TABLE_NAME_;
CREATE TABLE _TABLE_NAME_(_FIELDS_TYPE_);
INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_;
DROP TABLE _BACKUP_TABLE_NAME_;
`;
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`));
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
sql = sql.replace(/_FIELDS_NO_TYPE_/g, (this.escapeFields(fieldsNoType) as string[]).join(','));
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
return sql.trim().split('\n');
}
wrapQueries(queries: any[]) {
const output = [];
for (let i = 0; i < queries.length; i++) {
output.push(this.wrapQuery(queries[i]));
}
return output;
}
wrapQuery(sql: any, params: SqlParams = null): SqlQuery {
if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`);
if (Array.isArray(sql)) {
return {
sql: sql[0],
params: sql.length >= 2 ? sql[1] : null,
};
} else if (typeof sql === 'string') {
return { sql: sql, params: params };
} else {
return sql; // Already wrapped
}
}
}

View File

@@ -1,4 +1,4 @@
import JoplinServerApi from './JoplinServerApi2';
import JoplinServerApi from './JoplinServerApi';
const { dirname, basename } = require('./path-utils');
function removeTrailingColon(path: string) {

View File

@@ -198,7 +198,7 @@ class FileApiDriverOneDrive {
async clearRoot() {
const recurseItems = async (path) => {
const result = await this.list(this.fileApi_.fullPath_(path));
const result = await this.list(this.fileApi_.fullPath(path));
const output = [];
for (const item of result.items) {
@@ -206,7 +206,7 @@ class FileApiDriverOneDrive {
if (item.isDir) {
await recurseItems(fullPath);
}
await this.delete(this.fileApi_.fullPath_(fullPath));
await this.delete(this.fileApi_.fullPath(fullPath));
}
return output;

View File

@@ -1,16 +1,17 @@
import Logger from './Logger';
import shim from './shim';
import BaseItem from './models/BaseItem';
import time from './time';
const { isHidden } = require('./path-utils');
const Logger = require('./Logger').default;
const shim = require('./shim').default;
const BaseItem = require('./models/BaseItem').default;
const JoplinError = require('./JoplinError');
const ArrayUtils = require('./ArrayUtils');
const time = require('./time').default;
const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
const logger = Logger.create('FileApi');
function requestCanBeRepeated(error) {
function requestCanBeRepeated(error: any) {
const errorCode = typeof error === 'object' && error.code ? error.code : null;
// The target is explicitely rejecting the item so repeating wouldn't make a difference.
@@ -25,7 +26,7 @@ function requestCanBeRepeated(error) {
return true;
}
async function tryAndRepeat(fn, count) {
async function tryAndRepeat(fn: Function, count: number) {
let retryCount = 0;
// Don't use internal fetch retry mechanim since we
@@ -52,24 +53,28 @@ async function tryAndRepeat(fn, count) {
}
class FileApi {
constructor(baseDir, driver) {
private baseDir_: any;
private driver_: any;
private logger_: Logger = new Logger();
private syncTargetId_: number = null;
private tempDirName_: string = null;
public requestRepeatCount_: number = null; // For testing purpose only - normally this value should come from the driver
private remoteDateOffset_ = 0;
private remoteDateNextCheckTime_ = 0;
private remoteDateMutex_ = new Mutex();
private initialized_ = false;
constructor(baseDir: string | Function, driver: any) {
this.baseDir_ = baseDir;
this.driver_ = driver;
this.logger_ = new Logger();
this.syncTargetId_ = null;
this.tempDirName_ = null;
this.driver_.fileApi_ = this;
this.requestRepeatCount_ = null; // For testing purpose only - normally this value should come from the driver
this.remoteDateOffset_ = 0;
this.remoteDateNextCheckTime_ = 0;
this.remoteDateMutex_ = new Mutex();
this.initialized_ = false;
}
async initialize() {
if (this.initialized_) return;
this.initialized_ = true;
if (this.driver_.initialize) return this.driver_.initialize(this.fullPath_(''));
if (this.driver_.initialize) return this.driver_.initialize(this.fullPath(''));
}
async fetchRemoteDateOffset_() {
@@ -89,7 +94,7 @@ class FileApi {
if (!stat) throw new Error('Timed out trying to get sync target clock time');
this.delete(tempFile); // No need to await for this call
void this.delete(tempFile); // No need to await for this call
const endTime = Date.now();
const expectedTime = Math.round((endTime + startTime) / 2);
@@ -153,7 +158,7 @@ class FileApi {
return this.tempDirName_;
}
setTempDirName(v) {
setTempDirName(v: string) {
this.tempDirName_ = v;
}
@@ -165,7 +170,7 @@ class FileApi {
return this.driver_;
}
setSyncTargetId(v) {
setSyncTargetId(v: number) {
this.syncTargetId_ = v;
}
@@ -174,7 +179,7 @@ class FileApi {
return this.syncTargetId_;
}
setLogger(l) {
setLogger(l: Logger) {
if (!l) l = new Logger();
this.logger_ = l;
}
@@ -183,7 +188,7 @@ class FileApi {
return this.logger_;
}
fullPath_(path) {
fullPath(path: string) {
const output = [];
if (this.baseDir()) output.push(this.baseDir());
if (path) output.push(path);
@@ -192,7 +197,7 @@ class FileApi {
// DRIVER MUST RETURN PATHS RELATIVE TO `path`
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
async list(path = '', options = null) {
async list(path = '', options: any = null) {
if (!options) options = {};
if (!('includeHidden' in options)) options.includeHidden = false;
if (!('context' in options)) options.context = null;
@@ -201,7 +206,7 @@ class FileApi {
logger.debug(`list ${this.baseDir()}`);
const result = await tryAndRepeat(() => this.driver_.list(this.fullPath_(path), options), this.requestRepeatCount());
const result = await tryAndRepeat(() => this.driver_.list(this.fullPath(path), options), this.requestRepeatCount());
if (!options.includeHidden) {
const temp = [];
@@ -212,38 +217,38 @@ class FileApi {
}
if (!options.includeDirs) {
result.items = result.items.filter(f => !f.isDir);
result.items = result.items.filter((f: any) => !f.isDir);
}
if (options.syncItemsOnly) {
result.items = result.items.filter(f => !f.isDir && BaseItem.isSystemPath(f.path));
result.items = result.items.filter((f: any) => !f.isDir && BaseItem.isSystemPath(f.path));
}
return result;
}
// Deprectated
setTimestamp(path, timestampMs) {
logger.debug(`setTimestamp ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath_(path), timestampMs), this.requestRepeatCount());
// return this.driver_.setTimestamp(this.fullPath_(path), timestampMs);
setTimestamp(path: string, timestampMs: number) {
logger.debug(`setTimestamp ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.setTimestamp(this.fullPath(path), timestampMs), this.requestRepeatCount());
// return this.driver_.setTimestamp(this.fullPath(path), timestampMs);
}
mkdir(path) {
logger.debug(`mkdir ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath_(path)), this.requestRepeatCount());
mkdir(path: string) {
logger.debug(`mkdir ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.mkdir(this.fullPath(path)), this.requestRepeatCount());
}
async stat(path) {
logger.debug(`stat ${this.fullPath_(path)}`);
async stat(path: string) {
logger.debug(`stat ${this.fullPath(path)}`);
const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath_(path)), this.requestRepeatCount());
const output = await tryAndRepeat(() => this.driver_.stat(this.fullPath(path)), this.requestRepeatCount());
if (!output) return output;
output.path = path;
return output;
// return this.driver_.stat(this.fullPath_(path)).then((output) => {
// return this.driver_.stat(this.fullPath(path)).then((output) => {
// if (!output) return output;
// output.path = path;
// return output;
@@ -251,32 +256,32 @@ class FileApi {
}
// Returns UTF-8 encoded string by default, or a Response if `options.target = 'file'`
get(path, options = null) {
get(path: string, options: any = null) {
if (!options) options = {};
if (!options.encoding) options.encoding = 'utf8';
logger.debug(`get ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.get(this.fullPath_(path), options), this.requestRepeatCount());
logger.debug(`get ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.get(this.fullPath(path), options), this.requestRepeatCount());
}
async put(path, content, options = null) {
logger.debug(`put ${this.fullPath_(path)}`, options);
async put(path: string, content: any, options: any = null) {
logger.debug(`put ${this.fullPath(path)}`, options);
if (options && options.source === 'file') {
if (!(await this.fsDriver().exists(options.path))) throw new JoplinError(`File not found: ${options.path}`, 'fileNotFound');
}
return tryAndRepeat(() => this.driver_.put(this.fullPath_(path), content, options), this.requestRepeatCount());
return tryAndRepeat(() => this.driver_.put(this.fullPath(path), content, options), this.requestRepeatCount());
}
delete(path) {
logger.debug(`delete ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.delete(this.fullPath_(path)), this.requestRepeatCount());
delete(path: string) {
logger.debug(`delete ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.delete(this.fullPath(path)), this.requestRepeatCount());
}
// Deprectated
move(oldPath, newPath) {
logger.debug(`move ${this.fullPath_(oldPath)} => ${this.fullPath_(newPath)}`);
return tryAndRepeat(() => this.driver_.move(this.fullPath_(oldPath), this.fullPath_(newPath)), this.requestRepeatCount());
move(oldPath: string, newPath: string) {
logger.debug(`move ${this.fullPath(oldPath)} => ${this.fullPath(newPath)}`);
return tryAndRepeat(() => this.driver_.move(this.fullPath(oldPath), this.fullPath(newPath)), this.requestRepeatCount());
}
// Deprectated
@@ -288,14 +293,14 @@ class FileApi {
return tryAndRepeat(() => this.driver_.clearRoot(this.baseDir()), this.requestRepeatCount());
}
delta(path, options = null) {
logger.debug(`delta ${this.fullPath_(path)}`);
return tryAndRepeat(() => this.driver_.delta(this.fullPath_(path), options), this.requestRepeatCount());
delta(path: string, options: any = null) {
logger.debug(`delta ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount());
}
}
function basicDeltaContextFromOptions_(options) {
const output = {
function basicDeltaContextFromOptions_(options: any) {
const output: any = {
timestamp: 0,
filesAtTimestamp: [],
statsCache: null,
@@ -319,7 +324,7 @@ function basicDeltaContextFromOptions_(options) {
// This is the basic delta algorithm, which can be used in case the cloud service does not have
// a built-in delta API. OneDrive and Dropbox have one for example, but Nextcloud and obviously
// the file system do not.
async function basicDelta(path, getDirStatFn, options) {
async function basicDelta(path: string, getDirStatFn: Function, options: any) {
const outputLimit = 50;
const itemIds = await options.allItemIdsHandler();
if (!Array.isArray(itemIds)) throw new Error('Delta API not supported - local IDs must be provided');
@@ -344,10 +349,10 @@ async function basicDelta(path, getDirStatFn, options) {
// Stats are cached until all items have been processed (until hasMore is false)
if (newContext.statsCache === null) {
newContext.statsCache = await getDirStatFn(path);
newContext.statsCache.sort(function(a, b) {
newContext.statsCache.sort(function(a: any, b: any) {
return a.updated_time - b.updated_time;
});
newContext.statIdsCache = newContext.statsCache.filter(item => BaseItem.isSystemPath(item.path)).map(item => BaseItem.pathToId(item.path));
newContext.statIdsCache = newContext.statsCache.filter((item: any) => BaseItem.isSystemPath(item.path)).map((item: any) => BaseItem.pathToId(item.path));
newContext.statIdsCache.sort(); // Items must be sorted to use binary search below
}
@@ -449,4 +454,4 @@ async function basicDelta(path, getDirStatFn, options) {
};
}
module.exports = { FileApi, basicDelta };
export { FileApi, basicDelta };

View File

@@ -31,20 +31,19 @@ const selfClosingElements = [
];
class HtmlUtils {
public headAndBodyHtml(doc: any) {
headAndBodyHtml(doc) {
const output = [];
if (doc.head) output.push(doc.head.innerHTML);
if (doc.body) output.push(doc.body.innerHTML);
return output.join('\n');
}
public isSelfClosingTag(tagName: string) {
isSelfClosingTag(tagName) {
return selfClosingElements.includes(tagName.toLowerCase());
}
// Returns the **encoded** URLs, so to be useful they should be decoded again before use.
public extractImageUrls(html: string) {
extractImageUrls(html) {
if (!html) return [];
const output = [];
@@ -56,8 +55,8 @@ class HtmlUtils {
return output.filter(url => !!url);
}
public replaceImageUrls(html: string, callback: Function) {
return this.processImageTags(html, (data: any) => {
replaceImageUrls(html, callback) {
return this.processImageTags(html, data => {
const newSrc = callback(data.src);
return {
type: 'replaceSource',
@@ -66,10 +65,10 @@ class HtmlUtils {
});
}
public processImageTags(html: string, callback: Function) {
processImageTags(html, callback) {
if (!html) return '';
return html.replace(imageRegex, (_v: string, before: string, src: string, after: string) => {
return html.replace(imageRegex, (v, before, src, after) => {
const action = callback({ src: src });
if (!action) return `<img${before}src="${src}"${after}>`;
@@ -91,16 +90,16 @@ class HtmlUtils {
});
}
public prependBaseUrl(html: string, baseUrl: string) {
prependBaseUrl(html, baseUrl) {
if (!html) return '';
return html.replace(anchorRegex, (_v: string, before: string, href: string, after: string) => {
return html.replace(anchorRegex, (v, before, href, after) => {
const newHref = urlUtils.prependBaseUrl(href, baseUrl);
return `<a${before}href="${newHref}"${after}>`;
});
}
public attributesHtml(attr: any) {
attributesHtml(attr) {
const output = [];
for (const n in attr) {
@@ -111,10 +110,10 @@ class HtmlUtils {
return output.join(' ');
}
public stripHtml(html: string) {
const output: string[] = [];
stripHtml(html) {
const output = [];
const tagStack: any[] = [];
const tagStack = [];
const currentTag = () => {
if (!tagStack.length) return '';
@@ -125,16 +124,16 @@ class HtmlUtils {
const parser = new htmlparser2.Parser({
onopentag: (name: string) => {
onopentag: (name) => {
tagStack.push(name.toLowerCase());
},
ontext: (decodedText: string) => {
ontext: (decodedText) => {
if (disallowedTags.includes(currentTag())) return;
output.push(decodedText);
},
onclosetag: (name: string) => {
onclosetag: (name) => {
if (currentTag() === name.toLowerCase()) tagStack.pop();
},
@@ -147,4 +146,6 @@ class HtmlUtils {
}
}
export default new HtmlUtils();
const htmlUtils = new HtmlUtils();
module.exports = htmlUtils;

View File

@@ -1,7 +1,7 @@
const stringToStream = require('string-to-stream');
// const cleanHtml = require('clean-html');
const resourceUtils = require('./resourceUtils.js');
const htmlUtils = require('./htmlUtils').default;
const { isSelfClosingTag } = require('./htmlUtils');
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
@@ -126,7 +126,7 @@ function enexXmlToHtml_(stream, resources) {
const nodeAttributes = attributeToLowerCase(node);
const checkedHtml = nodeAttributes.checked && nodeAttributes.checked.toLowerCase() == 'true' ? ' checked="checked" ' : ' ';
section.lines.push(`<input${checkedHtml}type="checkbox" onclick="return false;" />`);
} else if (htmlUtils.isSelfClosingTag(tagName)) {
} else if (isSelfClosingTag(tagName)) {
section.lines.push(`<${tagName}${attributesStr}/>`);
} else {
section.lines.push(`<${tagName}${attributesStr}>`);
@@ -135,7 +135,7 @@ function enexXmlToHtml_(stream, resources) {
saxStream.on('closetag', function(node) {
const tagName = node ? node.toLowerCase() : node;
if (!htmlUtils.isSelfClosingTag(tagName)) section.lines.push(`</${tagName}>`);
if (!isSelfClosingTag(tagName)) section.lines.push(`</${tagName}>`);
});
saxStream.on('attribute', function() {});

View File

@@ -3,7 +3,7 @@ import Setting from './models/Setting';
import shim from './shim';
import MarkupToHtml, { MarkupLanguage } from '@joplin/renderer/MarkupToHtml';
import htmlUtils from './htmlUtils';
const htmlUtils = require('./htmlUtils');
import Resource from './models/Resource';
export class MarkupLanguageUtils {

View File

@@ -6,7 +6,7 @@ import time from '../time';
import markdownUtils from '../markdownUtils';
import { _ } from '../locale';
const { Database } = require('../database.js');
import Database from '../database';
import ItemChange from './ItemChange';
const JoplinError = require('../JoplinError.js');
const { sprintf } = require('sprintf-js');
@@ -115,7 +115,7 @@ export default class BaseItem extends BaseModel {
return r.total;
}
static systemPath(itemOrId: any, extension: string = null) {
public static systemPath(itemOrId: any, extension: string = null) {
if (extension === null) extension = 'md';
if (typeof itemOrId === 'string') return `${itemOrId}.${extension}`;
@@ -225,7 +225,7 @@ export default class BaseItem extends BaseModel {
// Don't create a deleted_items entry when conflicted notes are deleted
// since no other client have (or should have) them.
let conflictNoteIds = [];
let conflictNoteIds: string[] = [];
if (this.modelType() == BaseModel.TYPE_NOTE) {
const conflictNotes = await this.db().selectAll(`SELECT id FROM notes WHERE id IN ("${ids.join('","')}") AND is_conflict = 1`);
conflictNoteIds = conflictNotes.map((n: NoteEntity) => {

View File

@@ -4,7 +4,7 @@ import time from '../time';
import { _ } from '../locale';
import Note from './Note';
const { Database } = require('../database.js');
import Database from '../database';
import BaseItem from './BaseItem';
const { substrWithEllipsis } = require('../string-utils.js');
@@ -107,7 +107,7 @@ export default class Folder extends BaseItem {
return 'c04f1c7c04f1c7c04f1c7c04f1c7c04f';
}
static conflictFolder() {
static conflictFolder(): FolderEntity {
return {
type_: this.TYPE_FOLDER,
id: this.conflictFolderId(),
@@ -380,8 +380,8 @@ export default class Folder extends BaseItem {
return output;
}
static load(id: string) {
if (id == this.conflictFolderId()) return this.conflictFolder();
static load(id: string, _options: any = null): Promise<FolderEntity> {
if (id == this.conflictFolderId()) return Promise.resolve(this.conflictFolder());
return super.load(id);
}

View File

@@ -110,7 +110,7 @@ export default class Note extends BaseItem {
return BaseModel.TYPE_NOTE;
}
static linkedItemIds(body: string) {
static linkedItemIds(body: string): string[] {
if (!body || body.length <= 32) return [];
const links = urlUtils.extractResourceUrls(body);
@@ -319,7 +319,8 @@ export default class Note extends BaseItem {
static previewFieldsSql(fields: string[] = null) {
if (fields === null) fields = this.previewFields();
return this.db().escapeFields(fields).join(',');
const escaped = this.db().escapeFields(fields);
return Array.isArray(escaped) ? escaped.join(',') : escaped;
}
static async loadFolderNoteByField(folderId: string, field: string, value: any) {

View File

@@ -1,4 +1,5 @@
import BaseModel from '../BaseModel';
import { SqlQuery } from '../database';
// - If is_associated = 1, note_resources indicates which note_id is currently associated with the given resource_id
// - If is_associated = 0, note_resources indicates which note_id *was* associated with the given resource_id
@@ -14,6 +15,27 @@ export default class NoteResource extends BaseModel {
return BaseModel.TYPE_NOTE_RESOURCE;
}
public static async applySharedStatusToLinkedResources() {
const queries: SqlQuery[] = [];
queries.push({ sql: `
UPDATE resources
SET is_shared = 0
` });
queries.push({ sql: `
UPDATE resources
SET is_shared = 1
WHERE id IN (
SELECT DISTINCT note_resources.resource_id
FROM notes JOIN note_resources ON notes.id = note_resources.note_id
WHERE notes.is_shared = 1
)
` });
await this.db().transactionExecBatch(queries);
}
static async associatedNoteIds(resourceId: string): Promise<string[]> {
const rows = await this.modelSelectAll('SELECT note_id FROM note_resources WHERE resource_id = ? AND is_associated = 1', [resourceId]);
return rows.map((r: any) => r.note_id);

View File

@@ -42,11 +42,15 @@ export default class Resource extends BaseItem {
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
}
static fetchStatuses(resourceIds: string[]) {
if (!resourceIds.length) return [];
static fetchStatuses(resourceIds: string[]): Promise<any[]> {
if (!resourceIds.length) return Promise.resolve([]);
return this.db().selectAll(`SELECT resource_id, fetch_status FROM resource_local_states WHERE resource_id IN ("${resourceIds.join('","')}")`);
}
public static sharedResourceIds(): Promise<string[]> {
return this.db().selectAllFields('SELECT id FROM resources WHERE is_shared = 1', {}, 'id');
}
static errorFetchStatuses() {
return this.db().selectAll(`
SELECT title AS resource_title, resource_id, fetch_error

View File

@@ -1,6 +1,6 @@
import BaseModel from '../BaseModel';
import { ResourceLocalStateEntity } from '../services/database/types';
const { Database } = require('../database.js');
import Database from '../database';
export default class ResourceLocalState extends BaseModel {
static tableName() {

View File

@@ -3,7 +3,7 @@ import { _, supportedLocalesToLanguages, defaultLocale } from '../locale';
import { ltrimSlashes } from '../path-utils';
import eventManager from '../eventManager';
import BaseModel from '../BaseModel';
const { Database } = require('../database.js');
import Database from '../database';
const SyncTargetRegistry = require('../SyncTargetRegistry.js');
import time from '../time';
const { sprintf } = require('sprintf-js');

View File

@@ -1,233 +0,0 @@
const Logger = require('./Logger').default;
const Setting = require('./models/Setting').default;
const shim = require('./shim').default;
const SyncTargetRegistry = require('./SyncTargetRegistry.js');
const reg = {};
reg.syncTargets_ = {};
reg.logger = () => {
if (!reg.logger_) {
// console.warn('Calling logger before it is initialized');
return new Logger();
}
return reg.logger_;
};
reg.setLogger = l => {
reg.logger_ = l;
};
reg.setShowErrorMessageBoxHandler = v => {
reg.showErrorMessageBoxHandler_ = v;
};
reg.showErrorMessageBox = message => {
if (!reg.showErrorMessageBoxHandler_) return;
reg.showErrorMessageBoxHandler_(message);
};
reg.resetSyncTarget = (syncTargetId = null) => {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
delete reg.syncTargets_[syncTargetId];
};
reg.syncTargetNextcloud = () => {
return reg.syncTarget(SyncTargetRegistry.nameToId('nextcloud'));
};
reg.syncTarget = (syncTargetId = null) => {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
if (reg.syncTargets_[syncTargetId]) return reg.syncTargets_[syncTargetId];
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
if (!reg.db()) throw new Error('Cannot initialize sync without a db');
const target = new SyncTargetClass(reg.db());
target.setLogger(reg.logger());
reg.syncTargets_[syncTargetId] = target;
return target;
};
// This can be used when some data has been modified and we want to make
// sure it gets synced. So we wait for the current sync operation to
// finish (if one is running), then we trigger a sync just after.
reg.waitForSyncFinishedThenSync = async () => {
reg.waitForReSyncCalls_.push(true);
try {
const synchronizer = await reg.syncTarget().synchronizer();
await synchronizer.waitForSyncToFinish();
await reg.scheduleSync(0);
} finally {
reg.waitForReSyncCalls_.pop();
}
};
reg.scheduleSync = async (delay = null, syncOptions = null) => {
reg.schedSyncCalls_.push(true);
try {
if (delay === null) delay = 1000 * 10;
if (syncOptions === null) syncOptions = {};
let promiseResolve = null;
const promise = new Promise((resolve) => {
promiseResolve = resolve;
});
if (reg.scheduleSyncId_) {
shim.clearTimeout(reg.scheduleSyncId_);
reg.scheduleSyncId_ = null;
}
reg.logger().debug('Scheduling sync operation...', delay);
if (Setting.value('env') === 'dev' && delay !== 0) {
reg.logger().info('Schedule sync DISABLED!!!');
return;
}
const timeoutCallback = async () => {
reg.timerCallbackCalls_.push(true);
try {
reg.scheduleSyncId_ = null;
reg.logger().info('Preparing scheduled sync');
const syncTargetId = Setting.value('sync.target');
if (!(await reg.syncTarget(syncTargetId).isAuthenticated())) {
reg.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
promiseResolve();
return;
}
try {
const sync = await reg.syncTarget(syncTargetId).synchronizer();
const contextKey = `sync.${syncTargetId}.context`;
let context = Setting.value(contextKey);
try {
context = context ? JSON.parse(context) : {};
} catch (error) {
// Clearing the context is inefficient since it means all items are going to be re-downloaded
// however it won't result in duplicate items since the synchroniser is going to compare each
// item to the current state.
reg.logger().warn(`Could not parse JSON sync context ${contextKey}:`, context);
reg.logger().info('Clearing context and starting from scratch');
context = null;
}
try {
reg.logger().info('Starting scheduled sync');
const options = Object.assign({}, syncOptions, { context: context });
if (!options.saveContextHandler) {
options.saveContextHandler = newContext => {
Setting.setValue(contextKey, JSON.stringify(newContext));
};
}
const newContext = await sync.start(options);
Setting.setValue(contextKey, JSON.stringify(newContext));
} catch (error) {
if (error.code == 'alreadyStarted') {
reg.logger().info(error.message);
} else {
promiseResolve();
throw error;
}
}
} catch (error) {
reg.logger().info('Could not run background sync:');
reg.logger().info(error);
}
reg.setupRecurrentSync();
promiseResolve();
} finally {
reg.timerCallbackCalls_.pop();
}
};
if (delay === 0) {
timeoutCallback();
} else {
reg.scheduleSyncId_ = shim.setTimeout(timeoutCallback, delay);
}
return promise;
} finally {
reg.schedSyncCalls_.pop();
}
};
reg.setupRecurrentSync = () => {
reg.setupRecurrentCalls_.push(true);
try {
if (reg.recurrentSyncId_) {
shim.clearInterval(reg.recurrentSyncId_);
reg.recurrentSyncId_ = null;
}
if (!Setting.value('sync.interval')) {
reg.logger().debug('Recurrent sync is disabled');
} else {
reg.logger().debug(`Setting up recurrent sync with interval ${Setting.value('sync.interval')}`);
if (Setting.value('env') === 'dev') {
reg.logger().info('Recurrent sync operation DISABLED!!!');
return;
}
reg.recurrentSyncId_ = shim.setInterval(() => {
reg.logger().info('Running background sync on timer...');
reg.scheduleSync(0);
}, 1000 * Setting.value('sync.interval'));
}
} finally {
reg.setupRecurrentCalls_.pop();
}
};
reg.setDb = v => {
reg.db_ = v;
};
reg.db = () => {
return reg.db_;
};
reg.cancelTimers_ = () => {
if (this.recurrentSyncId_) {
shim.clearInterval(reg.recurrentSyncId_);
this.recurrentSyncId_ = null;
}
if (reg.scheduleSyncId_) {
shim.clearTimeout(reg.scheduleSyncId_);
reg.scheduleSyncId_ = null;
}
};
reg.cancelTimers = async () => {
reg.logger().info('Cancelling sync timers');
reg.cancelTimers_();
return new Promise((resolve) => {
shim.setInterval(() => {
// ensure processing complete
if (!reg.setupRecurrentCalls_.length && !reg.schedSyncCalls_.length && !reg.timerCallbackCalls_.length && !reg.waitForReSyncCalls_.length) {
reg.cancelTimers_();
resolve();
}
}, 100);
});
};
reg.syncCalls_ = [];
reg.schedSyncCalls_ = [];
reg.waitForReSyncCalls_ = [];
reg.setupRecurrentCalls_ = [];
reg.timerCallbackCalls_ = [];
module.exports = { reg };

241
packages/lib/registry.ts Normal file
View File

@@ -0,0 +1,241 @@
import Logger from './Logger';
import Setting from './models/Setting';
import shim from './shim';
const SyncTargetRegistry = require('./SyncTargetRegistry.js');
class Registry {
private syncTargets_: any = {};
private logger_: Logger = null;
private schedSyncCalls_: boolean[] = [];
private waitForReSyncCalls_: boolean[]= [];
private setupRecurrentCalls_: boolean[] = [];
private timerCallbackCalls_: boolean[] = [];
private showErrorMessageBoxHandler_: any;
private scheduleSyncId_: any;
private recurrentSyncId_: any;
private db_: any;
logger() {
if (!this.logger_) {
// console.warn('Calling logger before it is initialized');
return new Logger();
}
return this.logger_;
}
setLogger(l: Logger) {
this.logger_ = l;
}
setShowErrorMessageBoxHandler(v: any) {
this.showErrorMessageBoxHandler_ = v;
}
showErrorMessageBox(message: string) {
if (!this.showErrorMessageBoxHandler_) return;
this.showErrorMessageBoxHandler_(message);
}
resetSyncTarget(syncTargetId: number = null) {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
delete this.syncTargets_[syncTargetId];
}
syncTargetNextcloud() {
return this.syncTarget(SyncTargetRegistry.nameToId('nextcloud'));
}
syncTarget = (syncTargetId: number = null) => {
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
if (this.syncTargets_[syncTargetId]) return this.syncTargets_[syncTargetId];
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
if (!this.db()) throw new Error('Cannot initialize sync without a db');
const target = new SyncTargetClass(this.db());
target.setLogger(this.logger());
this.syncTargets_[syncTargetId] = target;
return target;
};
// This can be used when some data has been modified and we want to make
// sure it gets synced. So we wait for the current sync operation to
// finish (if one is running), then we trigger a sync just after.
waitForSyncFinishedThenSync = async () => {
this.waitForReSyncCalls_.push(true);
try {
const synchronizer = await this.syncTarget().synchronizer();
await synchronizer.waitForSyncToFinish();
await this.scheduleSync(0);
} finally {
this.waitForReSyncCalls_.pop();
}
};
scheduleSync = async (delay: number = null, syncOptions: any = null) => {
this.schedSyncCalls_.push(true);
try {
if (delay === null) delay = 1000 * 10;
if (syncOptions === null) syncOptions = {};
let promiseResolve: Function = null;
const promise = new Promise((resolve) => {
promiseResolve = resolve;
});
if (this.scheduleSyncId_) {
shim.clearTimeout(this.scheduleSyncId_);
this.scheduleSyncId_ = null;
}
this.logger().debug('Scheduling sync operation...', delay);
if (Setting.value('env') === 'dev' && delay !== 0) {
this.logger().info('Schedule sync DISABLED!!!');
return;
}
const timeoutCallback = async () => {
this.timerCallbackCalls_.push(true);
try {
this.scheduleSyncId_ = null;
this.logger().info('Preparing scheduled sync');
const syncTargetId = Setting.value('sync.target');
if (!(await this.syncTarget(syncTargetId).isAuthenticated())) {
this.logger().info('Synchroniser is missing credentials - manual sync required to authenticate.');
promiseResolve();
return;
}
try {
const sync = await this.syncTarget(syncTargetId).synchronizer();
const contextKey = `sync.${syncTargetId}.context`;
let context = Setting.value(contextKey);
try {
context = context ? JSON.parse(context) : {};
} catch (error) {
// Clearing the context is inefficient since it means all items are going to be re-downloaded
// however it won't result in duplicate items since the synchroniser is going to compare each
// item to the current state.
this.logger().warn(`Could not parse JSON sync context ${contextKey}:`, context);
this.logger().info('Clearing context and starting from scratch');
context = null;
}
try {
this.logger().info('Starting scheduled sync');
const options = Object.assign({}, syncOptions, { context: context });
if (!options.saveContextHandler) {
options.saveContextHandler = (newContext: any) => {
Setting.setValue(contextKey, JSON.stringify(newContext));
};
}
const newContext = await sync.start(options);
Setting.setValue(contextKey, JSON.stringify(newContext));
} catch (error) {
if (error.code == 'alreadyStarted') {
this.logger().info(error.message);
} else {
promiseResolve();
throw error;
}
}
} catch (error) {
this.logger().info('Could not run background sync:');
this.logger().info(error);
}
this.setupRecurrentSync();
promiseResolve();
} finally {
this.timerCallbackCalls_.pop();
}
};
if (delay === 0) {
void timeoutCallback();
} else {
this.scheduleSyncId_ = shim.setTimeout(timeoutCallback, delay);
}
return promise;
} finally {
this.schedSyncCalls_.pop();
}
};
setupRecurrentSync() {
this.setupRecurrentCalls_.push(true);
try {
if (this.recurrentSyncId_) {
shim.clearInterval(this.recurrentSyncId_);
this.recurrentSyncId_ = null;
}
if (!Setting.value('sync.interval')) {
this.logger().debug('Recurrent sync is disabled');
} else {
this.logger().debug(`Setting up recurrent sync with interval ${Setting.value('sync.interval')}`);
if (Setting.value('env') === 'dev') {
this.logger().info('Recurrent sync operation DISABLED!!!');
return;
}
this.recurrentSyncId_ = shim.setInterval(() => {
this.logger().info('Running background sync on timer...');
void this.scheduleSync(0);
}, 1000 * Setting.value('sync.interval'));
}
} finally {
this.setupRecurrentCalls_.pop();
}
}
setDb = (v: any) => {
this.db_ = v;
};
db() {
return this.db_;
}
cancelTimers_() {
if (this.recurrentSyncId_) {
shim.clearInterval(this.recurrentSyncId_);
this.recurrentSyncId_ = null;
}
if (this.scheduleSyncId_) {
shim.clearTimeout(this.scheduleSyncId_);
this.scheduleSyncId_ = null;
}
}
cancelTimers = async () => {
this.logger().info('Cancelling sync timers');
this.cancelTimers_();
return new Promise((resolve) => {
shim.setInterval(() => {
// ensure processing complete
if (!this.setupRecurrentCalls_.length && !this.schedSyncCalls_.length && !this.timerCallbackCalls_.length && !this.waitForReSyncCalls_.length) {
this.cancelTimers_();
resolve(null);
}
}, 100);
});
};
}
const reg = new Registry();
// eslint-disable-next-line import/prefer-default-export
export { reg };

View File

@@ -8,11 +8,13 @@ import Note from '../models/Note';
import Resource from '../models/Resource';
import SearchEngine from './searchengine/SearchEngine';
import ItemChangeUtils from './ItemChangeUtils';
import time from '../time';
const { sprintf } = require('sprintf-js');
export default class ResourceService extends BaseService {
public static isRunningInBackground_: boolean = false;
private isIndexing_: boolean = false;
private maintenanceCalls_: boolean[] = [];
private maintenanceTimer1_: any = null;
@@ -21,76 +23,89 @@ export default class ResourceService extends BaseService {
public async indexNoteResources() {
this.logger().info('ResourceService::indexNoteResources: Start');
await ItemChange.waitForAllSaved();
let foundNoteWithEncryption = false;
while (true) {
const changes = await ItemChange.modelSelectAll(`
SELECT id, item_id, type
FROM item_changes
WHERE item_type = ?
AND id > ?
ORDER BY id ASC
LIMIT 10
`,
[BaseModel.TYPE_NOTE, Setting.value('resourceService.lastProcessedChangeId')]
);
if (!changes.length) break;
const noteIds = changes.map((a: any) => a.item_id);
const notes = await Note.modelSelectAll(`SELECT id, title, body, encryption_applied FROM notes WHERE id IN ("${noteIds.join('","')}")`);
const noteById = (noteId: string) => {
for (let i = 0; i < notes.length; i++) {
if (notes[i].id === noteId) return notes[i];
}
// The note may have been deleted since the change was recorded. For example in this case:
// - Note created (Some Change object is recorded)
// - Note is deleted
// - ResourceService indexer runs.
// In that case, there will be a change for the note, but the note will be gone.
return null;
};
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
const note = noteById(change.item_id);
if (note) {
if (note.encryption_applied) {
// If we hit an encrypted note, abort processing for now.
// Note will eventually get decrypted and processing can resume then.
// This is a limitation of the change tracking system - we cannot skip a change
// and keep processing the rest since we only keep track of "lastProcessedChangeId".
foundNoteWithEncryption = true;
break;
}
await this.setAssociatedResources(note.id, note.body);
} else {
this.logger().warn(`ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ${change.item_id}`);
}
} else if (change.type === ItemChange.TYPE_DELETE) {
await NoteResource.remove(change.item_id);
} else {
throw new Error(`Invalid change type: ${change.type}`);
}
Setting.setValue('resourceService.lastProcessedChangeId', change.id);
}
if (foundNoteWithEncryption) break;
if (this.isIndexing_) {
this.logger().info('ResourceService::indexNoteResources: Already indexing - waiting for it to finish');
await time.waitTillCondition(() => !this.isIndexing_);
return;
}
await Setting.saveAll();
this.isIndexing_ = true;
await NoteResource.addOrphanedResources();
try {
await ItemChange.waitForAllSaved();
await ItemChangeUtils.deleteProcessedChanges();
let foundNoteWithEncryption = false;
while (true) {
const changes = await ItemChange.modelSelectAll(`
SELECT id, item_id, type
FROM item_changes
WHERE item_type = ?
AND id > ?
ORDER BY id ASC
LIMIT 10
`, [BaseModel.TYPE_NOTE, Setting.value('resourceService.lastProcessedChangeId')]
);
if (!changes.length) break;
const noteIds = changes.map((a: any) => a.item_id);
const notes = await Note.modelSelectAll(`SELECT id, title, body, encryption_applied FROM notes WHERE id IN ("${noteIds.join('","')}")`);
const noteById = (noteId: string) => {
for (let i = 0; i < notes.length; i++) {
if (notes[i].id === noteId) return notes[i];
}
// The note may have been deleted since the change was recorded. For example in this case:
// - Note created (Some Change object is recorded)
// - Note is deleted
// - ResourceService indexer runs.
// In that case, there will be a change for the note, but the note will be gone.
return null;
};
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
if (change.type === ItemChange.TYPE_CREATE || change.type === ItemChange.TYPE_UPDATE) {
const note = noteById(change.item_id);
if (note) {
if (note.encryption_applied) {
// If we hit an encrypted note, abort processing for now.
// Note will eventually get decrypted and processing can resume then.
// This is a limitation of the change tracking system - we cannot skip a change
// and keep processing the rest since we only keep track of "lastProcessedChangeId".
foundNoteWithEncryption = true;
break;
}
await this.setAssociatedResources(note.id, note.body);
} else {
this.logger().warn(`ResourceService::indexNoteResources: A change was recorded for a note that has been deleted: ${change.item_id}`);
}
} else if (change.type === ItemChange.TYPE_DELETE) {
await NoteResource.remove(change.item_id);
} else {
throw new Error(`Invalid change type: ${change.type}`);
}
Setting.setValue('resourceService.lastProcessedChangeId', change.id);
}
if (foundNoteWithEncryption) break;
}
await Setting.saveAll();
await NoteResource.addOrphanedResources();
await ItemChangeUtils.deleteProcessedChanges();
} catch (error) {
this.logger().error('ResourceService::indexNoteResources:', error);
}
this.isIndexing_ = false;
this.logger().info('ResourceService::indexNoteResources: Completed');
}
@@ -176,7 +191,7 @@ export default class ResourceService extends BaseService {
const iid = shim.setInterval(() => {
if (!this.maintenanceCalls_.length) {
shim.clearInterval(iid);
resolve();
resolve(null);
}
}, 100);
});

View File

@@ -6,6 +6,7 @@ export interface AlarmEntity {
'id'?: number | null;
'note_id'?: string;
'trigger_time'?: number;
'type_'?: number;
}
export interface DeletedItemEntity {
'id'?: number | null;
@@ -13,6 +14,7 @@ export interface DeletedItemEntity {
'item_id'?: string;
'deleted_time'?: number;
'sync_target'?: number;
'type_'?: number;
}
export interface FolderEntity {
'id'?: string | null;
@@ -25,6 +27,7 @@ export interface FolderEntity {
'encryption_applied'?: number;
'parent_id'?: string;
'is_shared'?: number;
'type_'?: number;
}
export interface ItemChangeEntity {
'id'?: number | null;
@@ -34,6 +37,7 @@ export interface ItemChangeEntity {
'created_time'?: number;
'source'?: number;
'before_change_item'?: string;
'type_'?: number;
}
export interface KeyValueEntity {
'id'?: number | null;
@@ -41,6 +45,7 @@ export interface KeyValueEntity {
'value'?: string;
'type'?: number;
'updated_time'?: number;
'type_'?: number;
}
export interface MasterKeyEntity {
'id'?: string | null;
@@ -50,12 +55,14 @@ export interface MasterKeyEntity {
'encryption_method'?: number;
'checksum'?: string;
'content'?: string;
'type_'?: number;
}
export interface MigrationEntity {
'id'?: number | null;
'number'?: number;
'updated_time'?: number;
'created_time'?: number;
'type_'?: number;
}
export interface NoteResourceEntity {
'id'?: number | null;
@@ -63,6 +70,7 @@ export interface NoteResourceEntity {
'resource_id'?: string;
'is_associated'?: number;
'last_seen_time'?: number;
'type_'?: number;
}
export interface NoteTagEntity {
'id'?: string | null;
@@ -75,6 +83,7 @@ export interface NoteTagEntity {
'encryption_cipher_text'?: string;
'encryption_applied'?: number;
'is_shared'?: number;
'type_'?: number;
}
export interface NoteEntity {
'id'?: string | null;
@@ -102,6 +111,7 @@ export interface NoteEntity {
'encryption_applied'?: number;
'markup_language'?: number;
'is_shared'?: number;
'type_'?: number;
}
export interface NotesNormalizedEntity {
'id'?: string;
@@ -116,12 +126,14 @@ export interface NotesNormalizedEntity {
'longitude'?: number;
'altitude'?: number;
'source_url'?: string;
'type_'?: number;
}
export interface ResourceLocalStateEntity {
'id'?: number | null;
'resource_id'?: string;
'fetch_status'?: number;
'fetch_error'?: string;
'type_'?: number;
}
export interface ResourceEntity {
'id'?: string | null;
@@ -138,12 +150,14 @@ export interface ResourceEntity {
'encryption_blob_encrypted'?: number;
'size'?: number;
'is_shared'?: number;
'type_'?: number;
}
export interface ResourcesToDownloadEntity {
'id'?: number | null;
'resource_id'?: string;
'updated_time'?: number;
'created_time'?: number;
'type_'?: number;
}
export interface RevisionEntity {
'id'?: string | null;
@@ -158,10 +172,12 @@ export interface RevisionEntity {
'encryption_applied'?: number;
'updated_time'?: number;
'created_time'?: number;
'type_'?: number;
}
export interface SettingEntity {
'key'?: string | null;
'value'?: string | null;
'type_'?: number;
}
export interface SyncItemEntity {
'id'?: number | null;
@@ -173,6 +189,7 @@ export interface SyncItemEntity {
'sync_disabled_reason'?: string;
'force_sync'?: number;
'item_location'?: number;
'type_'?: number;
}
export interface TableFieldEntity {
'id'?: number | null;
@@ -180,6 +197,7 @@ export interface TableFieldEntity {
'field_name'?: string;
'field_type'?: number;
'field_default'?: string | null;
'type_'?: number;
}
export interface TagEntity {
'id'?: string | null;
@@ -192,6 +210,7 @@ export interface TagEntity {
'encryption_applied'?: number;
'is_shared'?: number;
'parent_id'?: string;
'type_'?: number;
}
export interface TagsWithNoteCountEntity {
'id'?: string | null;
@@ -199,8 +218,10 @@ export interface TagsWithNoteCountEntity {
'created_time'?: number | null;
'updated_time'?: number | null;
'note_count'?: any | null;
'type_'?: number;
}
export interface VersionEntity {
'version'?: number;
'table_fields_version'?: number;
'type_'?: number;
}

View File

@@ -10,13 +10,13 @@ import { RequestMethod, Request } from '../Api';
import markdownUtils from '../../../markdownUtils';
import collectionToPaginatedResults from '../utils/collectionToPaginatedResults';
const { reg } = require('../../../registry.js');
const { Database } = require('../../../database.js');
import { reg } from '../../../registry';
import Database from '../../../database';
import Folder from '../../../models/Folder';
import Note from '../../../models/Note';
import Tag from '../../../models/Tag';
import Resource from '../../../models/Resource';
import htmlUtils from '../../../htmlUtils';
const htmlUtils = require('../../../htmlUtils');
import markupLanguageUtils from '../../../markupLanguageUtils';
const mimeUtils = require('../../../mime-utils.js').mime;
const md5 = require('md5');

View File

@@ -2,7 +2,7 @@ import shim from '../../../shim';
import MigrationHandler from '../MigrationHandler';
const { useEffect, useState } = shim.react();
import Setting from '../../../models/Setting';
const { reg } = require('../../../registry');
import { reg } from '../../../registry';
export interface SyncTargetUpgradeResult {
done: boolean;

View File

@@ -21,6 +21,14 @@ let react_: any = null;
const shim = {
Geolocation: null as any,
msleep_: (ms: number) => {
return new Promise((resolve: Function) => {
shim.setTimeout(() => {
resolve(null);
}, ms);
});
},
isNode: () => {
if (typeof process === 'undefined') return false;
if (shim.isElectron()) return true;
@@ -140,8 +148,6 @@ const shim = {
},
fetchWithRetry: async function(fetchFn: Function, options: any = null) {
const time = require('./time');
if (!options) options = {};
if (!options.timeout) options.timeout = 1000 * 120; // ms
if (!('maxRetry' in options)) options.maxRetry = shim.fetchMaxRetry_;
@@ -155,7 +161,7 @@ const shim = {
if (shim.fetchRequestCanBeRetried(error)) {
retryCount++;
if (retryCount > options.maxRetry) throw error;
await time.sleep(retryCount * 3);
await shim.msleep_(retryCount * 3000);
} else {
throw error;
}

View File

@@ -368,10 +368,9 @@ const themeCache_: any = {};
function themeStyle(themeId: number) {
if (!themeId) throw new Error('Theme must be specified');
const zoomRatio = 1; // Setting.value('style.zoom') / 100;
const editorFontSize = Setting.value('style.editor.fontSize');
const zoomRatio = 1;
const cacheKey = [themeId, zoomRatio, editorFontSize].join('-');
const cacheKey = themeId;
if (themeCache_[cacheKey]) return themeCache_[cacheKey];
// Font size are not theme specific, but they must be referenced
@@ -380,8 +379,6 @@ function themeStyle(themeId: number) {
const fontSizes: any = {
fontSize: Math.round(12 * zoomRatio),
toolbarIconSize: 18,
editorFontSize: editorFontSize,
textAreaLineHeight: Math.round(globalStyle.textAreaLineHeight * editorFontSize / 12),
};
fontSizes.noteViewerFontSize = Math.round(fontSizes.fontSize * 1.25);

View File

@@ -1,66 +1,68 @@
import shim from './shim';
const moment = require('moment');
type ConditionHandler = ()=> boolean;
class Time {
private dateFormat_: string = 'DD/MM/YYYY';
private timeFormat_: string = 'HH:mm';
private locale_: string = 'en-us';
locale() {
public locale() {
return this.locale_;
}
setLocale(v: string) {
public setLocale(v: string) {
moment.locale(v);
this.locale_ = v;
}
dateFormat() {
public dateFormat() {
return this.dateFormat_;
}
setDateFormat(v: string) {
public setDateFormat(v: string) {
this.dateFormat_ = v;
}
timeFormat() {
public timeFormat() {
return this.timeFormat_;
}
setTimeFormat(v: string) {
public setTimeFormat(v: string) {
this.timeFormat_ = v;
}
use24HourFormat() {
public use24HourFormat() {
return this.timeFormat() ? this.timeFormat().includes('HH') : true;
}
formatDateToLocal(date: Date, format: string = null) {
public formatDateToLocal(date: Date, format: string = null) {
return this.formatMsToLocal(date.getTime(), format);
}
dateTimeFormat() {
public dateTimeFormat() {
return `${this.dateFormat()} ${this.timeFormat()}`;
}
unix() {
public unix() {
return Math.floor(Date.now() / 1000);
}
unixMs() {
public unixMs() {
return Date.now();
}
unixMsToObject(ms: number) {
public unixMsToObject(ms: number) {
return new Date(ms);
}
unixMsToS(ms: number) {
public unixMsToS(ms: number) {
return Math.floor(ms / 1000);
}
unixMsToIso(ms: number) {
public unixMsToIso(ms: number) {
return (
`${moment
.unix(ms / 1000)
@@ -69,7 +71,7 @@ class Time {
);
}
unixMsToIsoSec(ms: number) {
public unixMsToIsoSec(ms: number) {
return (
`${moment
.unix(ms / 1000)
@@ -78,20 +80,20 @@ class Time {
);
}
unixMsToLocalDateTime(ms: number) {
public unixMsToLocalDateTime(ms: number) {
return moment.unix(ms / 1000).format('DD/MM/YYYY HH:mm');
}
unixMsToLocalHms(ms: number) {
public unixMsToLocalHms(ms: number) {
return moment.unix(ms / 1000).format('HH:mm:ss');
}
formatMsToLocal(ms: number, format: string = null) {
public formatMsToLocal(ms: number, format: string = null) {
if (format === null) format = this.dateTimeFormat();
return moment(ms).format(format);
}
formatLocalToMs(localDateTime: any, format: string = null) {
public formatLocalToMs(localDateTime: any, format: string = null) {
if (format === null) format = this.dateTimeFormat();
const m = moment(localDateTime, format);
if (m.isValid()) return m.toDate().getTime();
@@ -99,7 +101,7 @@ class Time {
}
// Mostly used as a utility function for the DateTime Electron component
anythingToDateTime(o: any, defaultValue: Date = null) {
public anythingToDateTime(o: any, defaultValue: Date = null) {
if (o && o.toDate) return o.toDate();
if (!o) return defaultValue;
let m = moment(o, time.dateTimeFormat());
@@ -108,7 +110,7 @@ class Time {
return m.isValid() ? m.toDate() : defaultValue;
}
msleep(ms: number) {
public msleep(ms: number) {
return new Promise((resolve: Function) => {
shim.setTimeout(() => {
resolve();
@@ -116,20 +118,33 @@ class Time {
});
}
sleep(seconds: number) {
public sleep(seconds: number) {
return this.msleep(seconds * 1000);
}
goBackInTime(startDate: any, n: number, period: any) {
public goBackInTime(startDate: any, n: number, period: any) {
// period is a string (eg. "day", "week", "month", "year" ), n is an integer
return moment(startDate).startOf(period).subtract(n, period).format('x');
}
goForwardInTime(startDate: any, n: number, period: any) {
public goForwardInTime(startDate: any, n: number, period: any) {
return moment(startDate).startOf(period).add(n, period).format('x');
}
public async waitTillCondition(condition: ConditionHandler) {
if (condition()) return;
return new Promise(resolve => {
const iid = setInterval(() => {
if (condition()) {
clearInterval(iid);
resolve(null);
}
}, 1000);
});
}
}
const time = new Time();

View File

@@ -1,6 +1,6 @@
import { _ } from './locale';
import Setting from './models/Setting';
const { reg } = require('./registry.js');
import { reg } from './registry';
export default function versionInfo(packageInfo: any) {
const p = packageInfo;

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import htmlUtils from './htmlUtils';
import linkReplacement from './MdToHtml/linkReplacement';
import utils from './utils';
import utils, { ItemIdToUrlHandler } from './utils';
// TODO: fix
// import Setting from '@joplin/lib/models/Setting';
@@ -32,6 +32,7 @@ interface RenderOptions {
resources: any;
postMessageSyntax: string;
enableLongPress: boolean;
itemIdToUrl?: ItemIdToUrlHandler;
}
interface RenderResult {
@@ -39,6 +40,13 @@ interface RenderResult {
pluginAssets: any[];
}
// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
function trimStart(s: string): string {
// eslint-disable-next-line no-control-regex
const startWhitespace = /^[\x09\x0A\x0B\x0C\x0D\x20\xA0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F\u205F\u3000\u2028\u2029\uFEFF]*/;
return s.replace(startWhitespace, '');
}
export default class HtmlToHtml {
private resourceBaseUrl_;
@@ -73,7 +81,7 @@ export default class HtmlToHtml {
}
splitHtml(html: string) {
const trimmedHtml = html.trimStart();
const trimmedHtml = trimStart(html);
if (trimmedHtml.indexOf('<style>') !== 0) return { html: html, css: '' };
const closingIndex = trimmedHtml.indexOf('</style>');
@@ -109,7 +117,7 @@ export default class HtmlToHtml {
html = htmlUtils.processImageTags(html, (data: any) => {
if (!data.src) return null;
const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_);
const r = utils.imageReplacement(this.ResourceModel_, data.src, options.resources, this.resourceBaseUrl_, options.itemIdToUrl);
if (!r) return null;
if (typeof r === 'string') {

View File

@@ -8,6 +8,18 @@ export enum MarkupLanguage {
Html = 2,
}
export interface RenderResultPluginAsset {
name: string;
path: string;
mime: string;
}
export interface RenderResult {
html: string;
pluginAssets: RenderResultPluginAsset[];
cssStrings: string[];
}
export default class MarkupToHtml {
static MARKUP_LANGUAGE_MARKDOWN: number = MarkupLanguage.Markdown;
@@ -75,7 +87,7 @@ export default class MarkupToHtml {
if (r.clearCache) r.clearCache();
}
async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any) {
async render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any): Promise<RenderResult> {
return this.renderer(markupLanguage).render(markup, theme, options);
}

View File

@@ -3,6 +3,8 @@ import noteStyle from './noteStyle';
import { fileExtension } from './pathUtils';
import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks';
import { ItemIdToUrlHandler } from './utils';
import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
const MarkdownIt = require('markdown-it');
const md5 = require('md5');
@@ -114,18 +116,6 @@ interface PluginContext {
currentLinks: Link[];
}
interface RenderResultPluginAsset {
name: string;
path: string;
mime: string;
}
interface RenderResult {
html: string;
pluginAssets: RenderResultPluginAsset[];
cssStrings: string[];
}
export interface RuleOptions {
context: PluginContext;
theme: any;
@@ -157,6 +147,8 @@ export interface RuleOptions {
audioPlayerEnabled: boolean;
videoPlayerEnabled: boolean;
pdfViewerEnabled: boolean;
itemIdToUrl?: ItemIdToUrlHandler;
}
export default class MdToHtml {

View File

@@ -1,4 +1,4 @@
import utils from '../utils';
import utils, { ItemIdToUrlHandler } from '../utils';
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode;
const urlUtils = require('../urlUtils.js');
@@ -12,6 +12,7 @@ export interface Options {
plainResourceRendering?: boolean;
postMessageSyntax?: string;
enableLongPress?: boolean;
itemIdToUrl?: ItemIdToUrlHandler;
}
export interface LinkReplacementResult {
@@ -65,6 +66,8 @@ export default function(href: string, options: Options = null): LinkReplacementR
resourceFullPath: null,
};
} else {
// If we are rendering a note link, we'll get here too, so in that
// case "resourceId" would actually be the note ID.
href = `joplin://${resourceId}`;
if (resourceHrefInfo.hash) href += `#${resourceHrefInfo.hash}`;
resourceIdAttr = `data-resource-id='${resourceId}'`;
@@ -109,7 +112,13 @@ export default function(href: string, options: Options = null): LinkReplacementR
if (title) attrHtml.push(`title='${htmlentities(title)}'`);
if (mime) attrHtml.push(`type='${htmlentities(mime)}'`);
if (options.plainResourceRendering || options.linkRenderingType === 2) {
let resourceFullPath = resource && options?.ResourceModel?.fullPath ? options.ResourceModel.fullPath(resource) : null;
if (resourceId && options.itemIdToUrl) {
const url = options.itemIdToUrl(resourceId);
attrHtml.push(`href='${htmlentities(url)}'`);
resourceFullPath = url;
} else if (options.plainResourceRendering || options.linkRenderingType === 2) {
icon = '';
attrHtml.push(`href='${htmlentities(href)}'`);
} else {
@@ -121,6 +130,6 @@ export default function(href: string, options: Options = null): LinkReplacementR
html: `<a ${attrHtml.join(' ')}>${icon}`,
resourceReady: true,
resource,
resourceFullPath: resource && options?.ResourceModel?.fullPath ? options.ResourceModel.fullPath(resource) : null,
resourceFullPath: resourceFullPath,
};
}

View File

@@ -9,12 +9,17 @@ export interface Options {
pdfViewerEnabled: boolean;
}
function resourceUrl(resourceFullPath: string): string {
if (resourceFullPath.indexOf('http://') === 0 || resourceFullPath.indexOf('https://')) return resourceFullPath;
return `file://${toForwardSlashes(resourceFullPath)}`;
}
export default function(link: Link, options: Options) {
const resource = link.resource;
if (!link.resourceReady || !resource || !resource.mime) return '';
const escapedResourcePath = htmlentities(`file://${toForwardSlashes(link.resourceFullPath)}`);
const escapedResourcePath = htmlentities(resourceUrl(link.resourceFullPath));
const escapedMime = htmlentities(resource.mime);
if (options.videoPlayerEnabled && resource.mime.indexOf('video/') === 0) {

View File

@@ -3,7 +3,7 @@ import htmlUtils from '../../htmlUtils';
import utils from '../../utils';
function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) {
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
if (typeof r === 'string') return r;
if (r) return `<img ${before} ${htmlUtils.attributesHtml(r)} ${after}/>`;
return `[Image: ${src}]`;

View File

@@ -14,7 +14,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
if (!Resource.isResourceUrl(src) || ruleOptions.plainResourceRendering) return defaultRender(tokens, idx, options, env, self);
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl);
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
if (typeof r === 'string') return r;
if (r) {
let js = '';

View File

@@ -20,6 +20,7 @@ function plugin(markdownIt: any, ruleOptions: RuleOptions) {
plainResourceRendering: ruleOptions.plainResourceRendering,
postMessageSyntax: ruleOptions.postMessageSyntax,
enableLongPress: ruleOptions.enableLongPress,
itemIdToUrl: ruleOptions.itemIdToUrl,
});
ruleOptions.context.currentLinks.push({

View File

@@ -57,7 +57,7 @@ function loadPluginAssets(assets) {
Whenever updating a Markdown-it plugin, such as Katex or Mermaid, make sure to run `npm run buildAssets`, which will compile the CSS and JS for use in the Joplin applications.
### Adding asset files
### Adding asset files
A plugin (or rule) can have any number of assets, such as CSS or font files, associated with it. To add an asset to a plugin, follow these steps:

View File

@@ -126,10 +126,15 @@ export default function(theme: any) {
margin-top: 0.2em;
margin-bottom: 0;
}
a[data-resource-id] {
display: flex;
flex-direction: row;
align-items: center;
}
.resource-icon {
display: inline-block;
position: relative;
top: .5em;
text-decoration: none;
width: 1.2em;
height: 1.4em;

View File

@@ -122,7 +122,9 @@ utils.resourceStatus = function(ResourceModel: any, resourceInfo: any) {
return resourceStatus;
};
utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string) {
export type ItemIdToUrlHandler = (resource: any)=> string;
utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
if (!ResourceModel || !resources) return null;
if (!ResourceModel.isResourceUrl(src)) return null;
@@ -136,12 +138,29 @@ utils.imageReplacement = function(ResourceModel: any, src: string, resources: an
const icon = utils.resourceStatusImage(resourceStatus);
return `<div class="not-loaded-resource resource-status-${resourceStatus}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>` + '</div>';
}
const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (ResourceModel.isSupportedImageMimeType(mime)) {
let newSrc = `./${ResourceModel.filename(resource)}`;
if (resourceBaseUrl) newSrc = resourceBaseUrl + newSrc;
newSrc += `?t=${resource.updated_time}`;
let newSrc = '';
if (itemIdToUrl) {
newSrc = itemIdToUrl(resource.id);
} else {
const temp = [];
if (resourceBaseUrl) {
temp.push(resourceBaseUrl);
} else {
temp.push('./');
}
temp.push(ResourceModel.filename(resource));
temp.push(`?t=${resource.updated_time}`);
newSrc = temp.join('');
}
// let newSrc = `./${ResourceModel.filename(resource)}`;
// newSrc += `?t=${resource.updated_time}`;
return {
'data-resource-id': resource.id,
src: newSrc,

View File

@@ -1,4 +1,4 @@
{
"verbose": true,
"watch": ["dist/"]
"watch": ["dist/", "../renderer", "../lib"]
}

View File

@@ -5,7 +5,7 @@
"scripts": {
"start-dev": "nodemon --config nodemon.json dist/app.js --env dev",
"start": "node dist/app.js",
"generate-types": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generate-types.js && rm -f db-buildTypes.sqlite",
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-db --env buildTypes && node dist/tools/generateTypes.js && rm -f db-buildTypes.sqlite",
"tsc": "tsc --project tsconfig.json",
"test": "jest",
"test-ci": "npm run test",
@@ -14,6 +14,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@joplin/lib": "^1.0.9",
"@joplin/renderer": "^1.7.4",
"bcryptjs": "^2.4.3",
"bulma": "^0.9.1",
"bulma-prefers-dark": "^0.1.0-beta.0",

View File

@@ -18,8 +18,19 @@ input.form-control {
align-items: center;
}
.navbar .logo-text {
font-size: 2.2em;
font-weight: bold;
margin-left: 0.5em;
}
/*
.navbar .logo {
height: 50px;
} */
.navbar .navbar-item img {
max-height: 3em;
}
.main {

Some files were not shown because too many files have changed in this diff Show More