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

Compare commits

...

38 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
Laurent Cozic
2f8aafd85b Releasing sub-packages 2021-01-27 23:59:02 +00:00
Laurent Cozic
95253f70ea Plugin Repo: Add link to plugin commit in commit message 2021-01-27 23:57:08 +00:00
Laurent Cozic
4156b13c32 Desktop: Fixed plugin panel issue that could crash app in rare case
Ref: https://discourse.joplinapp.org/t/rearranging-the-pannels-crushed-the-app-and-generated-fatal-error/14373
2021-01-27 22:52:54 +00:00
Laurent Cozic
18dfc5a987 Desktop release v1.7.9 2021-01-27 22:13:10 +00:00
Laurent Cozic
ae29238592 Desktop release v1.7.8 2021-01-27 22:12:05 +00:00
Laurent Cozic
99445cac27 Tools: Make sure notarization does not make Travis timeout the task 2021-01-27 22:11:40 +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
170 changed files with 3757 additions and 2175 deletions

View File

@@ -97,6 +97,9 @@ 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
@@ -784,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
@@ -817,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
@@ -829,6 +838,9 @@ 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
@@ -916,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
@@ -1333,6 +1348,12 @@ packages/plugin-repo-cli/lib/errorsHaveChanged.js.map
packages/plugin-repo-cli/lib/errorsHaveChanged.test.d.ts
packages/plugin-repo-cli/lib/errorsHaveChanged.test.js
packages/plugin-repo-cli/lib/errorsHaveChanged.test.js.map
packages/plugin-repo-cli/lib/gitCompareUrl.d.ts
packages/plugin-repo-cli/lib/gitCompareUrl.js
packages/plugin-repo-cli/lib/gitCompareUrl.js.map
packages/plugin-repo-cli/lib/gitCompareUrl.test.d.ts
packages/plugin-repo-cli/lib/gitCompareUrl.test.js
packages/plugin-repo-cli/lib/gitCompareUrl.test.js.map
packages/plugin-repo-cli/lib/types.d.ts
packages/plugin-repo-cli/lib/types.js
packages/plugin-repo-cli/lib/types.js.map
@@ -1426,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

243
.gitignore vendored
View File

@@ -84,6 +84,9 @@ 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
@@ -771,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
@@ -804,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
@@ -816,6 +825,9 @@ 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
@@ -903,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
@@ -1320,6 +1335,12 @@ packages/plugin-repo-cli/lib/errorsHaveChanged.js.map
packages/plugin-repo-cli/lib/errorsHaveChanged.test.d.ts
packages/plugin-repo-cli/lib/errorsHaveChanged.test.js
packages/plugin-repo-cli/lib/errorsHaveChanged.test.js.map
packages/plugin-repo-cli/lib/gitCompareUrl.d.ts
packages/plugin-repo-cli/lib/gitCompareUrl.js
packages/plugin-repo-cli/lib/gitCompareUrl.js.map
packages/plugin-repo-cli/lib/gitCompareUrl.test.d.ts
packages/plugin-repo-cli/lib/gitCompareUrl.test.js
packages/plugin-repo-cli/lib/gitCompareUrl.test.js.map
packages/plugin-repo-cli/lib/types.d.ts
packages/plugin-repo-cli/lib/types.js
packages/plugin-repo-cli/lib/types.js.map
@@ -1413,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

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

@@ -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

@@ -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

@@ -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

@@ -572,6 +572,15 @@ class MainScreenComponent extends React.Component<Props, State> {
}
resizableLayout_renderItem(key: string, event: any) {
// Key should never be undefined but somehow it can happen, also not
// clear how. For now in this case render nothing so that the app
// doesn't crash.
// https://discourse.joplinapp.org/t/rearranging-the-pannels-crushed-the-app-and-generated-fatal-error/14373?u=laurent
if (!key) {
console.error('resizableLayout_renderItem: Trying to render an item using an empty key. Full layout is:', this.props.mainLayout);
return null;
}
const eventEmitter = event.eventEmitter;
// const viewsToRemove:string[] = [];

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,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.7",
"version": "1.7.9",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

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

View File

@@ -45,6 +45,13 @@ module.exports = async function(params) {
console.log(`Notarizing ${appId} found at ${appPath}`);
// Every x seconds we print something to stdout, otherwise Travis will
// timeout the task after 10 minutes, and Apple notarization can take more
// time.
const waitingIntervalId = setInterval(() => {
console.log('.');
}, 60000);
await electron_notarize.notarize({
appBundleId: appId,
appPath: appPath,
@@ -66,6 +73,8 @@ module.exports = async function(params) {
ascProvider: process.env.APPLE_ASC_PROVIDER,
});
clearInterval(waitingIntervalId);
// It appears that electron-notarize doesn't staple the app, but without
// this we were still getting the malware warning when launching the app.
// Stapling the app means attaching the notarization ticket to it, so that

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

@@ -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

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-htmlparser2",
"version": "4.1.21",
"version": "4.1.22",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,7 +1,7 @@
{
"name": "@joplin/fork-htmlparser2",
"description": "Fast & forgiving HTML/XML/RSS parser",
"version": "4.1.21",
"version": "4.1.22",
"author": "Felix Boehm <me@feedic.com>",
"publishConfig": {
"access": "public"

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/fork-sax",
"version": "1.2.25",
"version": "1.2.26",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -2,7 +2,7 @@
"name": "@joplin/fork-sax",
"description": "An evented streaming XML parser in JavaScript",
"author": "Isaac Z. Schlueter <i@izs.me> (http://blog.izs.me/)",
"version": "1.2.25",
"version": "1.2.26",
"main": "lib/sax.js",
"publishConfig": {
"access": "public"

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

@@ -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,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "1.7.4",
"version": "1.7.5",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/lib",
"version": "1.7.4",
"version": "1.7.5",
"description": "Joplin Core library",
"author": "Laurent Cozic",
"homepage": "",
@@ -22,11 +22,11 @@
"typescript": "^4.0.5"
},
"dependencies": {
"@joplin/fork-htmlparser2": "^4.1.21",
"@joplin/fork-sax": "^1.2.25",
"@joplin/renderer": "^1.7.4",
"@joplin/turndown": "^4.0.43",
"@joplin/turndown-plugin-gfm": "^1.0.25",
"@joplin/fork-htmlparser2": "^4.1.22",
"@joplin/fork-sax": "^1.2.26",
"@joplin/renderer": "^1.7.5",
"@joplin/turndown": "^4.0.44",
"@joplin/turndown-plugin-gfm": "^1.0.26",
"async-mutex": "^0.1.3",
"aws-sdk": "^2.588.0",
"base-64": "^0.1.0",

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,8 +10,8 @@ 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';

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;

View File

@@ -9,6 +9,7 @@ import checkIfPluginCanBeAdded from './lib/checkIfPluginCanBeAdded';
import updateReadme from './lib/updateReadme';
import { ImportErrors, NpmPackage } from './lib/types';
import errorsHaveChanged from './lib/errorsHaveChanged';
import gitCompareUrl from './lib/gitCompareUrl';
function stripOffPackageOrg(name: string): string {
const n = name.split('/');
@@ -103,7 +104,7 @@ enum ProcessingActionType {
Update = 2,
}
function commitMessage(actionType: ProcessingActionType, manifest: any, npmPackage: NpmPackage, error: any): string {
function commitMessage(actionType: ProcessingActionType, manifest: any, previousManifest: any, npmPackage: NpmPackage, error: any): string {
const output: string[] = [];
if (!error) {
@@ -118,7 +119,9 @@ function commitMessage(actionType: ProcessingActionType, manifest: any, npmPacka
output.push(`Error: ${npmPackage.name}@${npmPackage.version}`);
}
return output.join(': ');
const compareUrl = gitCompareUrl(manifest, previousManifest);
return output.join(': ') + (compareUrl ? `\n\n${compareUrl}` : '');
}
function pluginManifestsPath(repoDir: string): string {
@@ -170,6 +173,7 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
let manifests: any = {};
let manifest: any = {};
let error: any = null;
let previousManifest: any = null;
try {
const destDir = `${repoDir}/plugins/`;
@@ -179,7 +183,10 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
actionType = ProcessingActionType.Add;
}
if (!obsoleteManifests[manifest.id]) manifests[manifest.id] = manifest;
if (!obsoleteManifests[manifest.id]) {
previousManifest = { ...manifests[manifest.id] };
manifests[manifest.id] = manifest;
}
} catch (e) {
console.error(e);
errors[npmPackage.name] = e.message || '';
@@ -213,7 +220,7 @@ async function processNpmPackage(npmPackage: NpmPackage, repoDir: string) {
if (!(await gitRepoClean())) {
await execCommand2('git add -A', { showOutput: false });
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, manifest, npmPackage, error)], { showOutput: false });
await execCommand2(['git', 'commit', '-m', commitMessage(actionType, manifest, previousManifest, npmPackage, error)], { showOutput: false });
} else {
console.info('Nothing to commit');
}

View File

@@ -0,0 +1,42 @@
import gitCompareUrl from './gitCompareUrl';
describe('gitCompareUrl', () => {
test('should create a compare URL', () => {
const testCases = [
[
{
repository_url: 'https://github.com/JackGruber/joplin-plugin-copytags',
_publish_commit: 'master:9ec4a476a54440ac43422c34e179dcabfca1e5a0',
},
{
repository_url: 'https://github.com/JackGruber/joplin-plugin-copytags',
_publish_commit: 'master:b52b01f6d3b709a811ac214253636a7c207c87dd',
},
'https://github.com/JackGruber/joplin-plugin-copytags/compare/b52b01f6d3b709a811ac214253636a7c207c87dd..9ec4a476a54440ac43422c34e179dcabfca1e5a0',
],
[
{
repository_url: 'https://github.com/JackGruber/joplin-plugin-copytags',
_publish_commit: 'master:9ec4a476a54440ac43422c34e179dcabfca1e5a0',
},
null,
'https://github.com/JackGruber/joplin-plugin-copytags/tree/9ec4a476a54440ac43422c34e179dcabfca1e5a0',
],
[
{
_publish_commit: 'master:9ec4a476a54440ac43422c34e179dcabfca1e5a0',
},
null,
null,
],
];
for (const t of testCases) {
const [manifest, previousManifest, expected] = t;
const actual = gitCompareUrl(manifest, previousManifest);
expect(actual).toBe(expected);
}
});
});

View File

@@ -0,0 +1,29 @@
import { rtrimSlashes } from '@joplin/lib/path-utils';
function removeBranch(commit: string): string {
if (!commit) return '';
if (commit.indexOf(':') >= 0) {
const s = commit.split(':');
return s[1];
}
return commit;
}
export default function(manifest: any, previousManifest: any = null): string {
// "repository_url": "https://github.com/JackGruber/joplin-plugin-copytags",
// "_publish_commit": "master:b52b01f6d3b709a811ac214253636a7c207c87dd",
// https://github.com/JackGruber/joplin-plugin-copytags/compare/9ec4a476a54440ac43422c34e179dcabfca1e5a0..b52b01f6d3b709a811ac214253636a7c207c87dd
const repoUrl: string = manifest.repository_url;
const commit: string = removeBranch(manifest._publish_commit);
const previousCommit: string = previousManifest ? removeBranch(previousManifest._publish_commit) : '';
if (!repoUrl) return null;
if (!commit && !previousCommit) return repoUrl;
if (commit && !previousCommit) return `${rtrimSlashes(repoUrl)}/tree/${commit}`;
return `${rtrimSlashes(repoUrl)}/compare/${previousCommit}..${commit}`;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "1.7.6",
"version": "1.7.7",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/plugin-repo-cli",
"version": "1.7.6",
"version": "1.7.7",
"description": "",
"main": "index.js",
"bin": {
@@ -18,8 +18,8 @@
"author": "",
"license": "MIT",
"dependencies": {
"@joplin/lib": "^1.7.4",
"@joplin/tools": "^1.7.4",
"@joplin/lib": "^1.7.5",
"@joplin/tools": "^1.7.5",
"fs-extra": "^9.0.1",
"yargs": "^16.0.3"
},

View File

@@ -29,6 +29,8 @@ The main two files you will want to look at are:
- `/src/index.ts`, which contains the entry point for the plugin source code.
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.
The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.
## Building the plugin
The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.
@@ -49,44 +51,21 @@ In general all this is done automatically by the plugin generator, which will se
## Updating the plugin framework
To update the plugin framework, run `yo joplin --update`
To update the plugin framework, run `npm run update`.
Keep in mind that doing so will overwrite all the framework-related files **outside of the "src/" directory** (your source code will not be touched). So if you have modified any of the framework-related files, such as package.json or .gitignore, make sure your code is under version control so that you can check the diff and re-apply your changes.
In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.
For that reason, it's generally best not to change any of the framework files or to do so in a way that minimises the number of changes. For example, if you want to modify the Webpack config, create a new separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.
## Content scripts
## External script files
A plugin that uses [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinplugins.html#registercontentscript) must declare them under the `content_scripts` key of [manifest.json](https://joplinapp.org/api/references/plugin_manifest/).
By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:
Each entry must be a path **relative to /src**, and **without extension**. The extension should not be included because it might change once the script is compiled. Each of these scripts will then be compiled to JavaScript and packaged into the plugin file. The content script files can be TypeScript (.ts or .tsx) or JavaScript.
- The script is a TypeScript file - in which case it has to be compiled to JavaScript.
For example, assuming these files:
- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.
```bash
/src
index.ts # Main plugin script
myContentScript.js # One content script (JS)
otherContentScript.ts # Another content script (TypeScript)
vendor/
test.ts # Sub-directories are also supported
```
The `manifest.json` file would be:
```json
{
"manifest_version": 1,
"name": "Testing Content Scripts",
content_scripts: [
"myContentScript",
"otherContentScript",
"vendor/test"
]
}
```
Note in particular how the file path is relative to /src and the extensions removed.
To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.
## License

View File

@@ -7,6 +7,7 @@ import JoplinCommands from './JoplinCommands';
import JoplinViews from './JoplinViews';
import JoplinInterop from './JoplinInterop';
import JoplinSettings from './JoplinSettings';
import JoplinContentScripts from './JoplinContentScripts';
/**
* This is the main entry point to the Joplin API. You can access various services using the provided accessors.
*
@@ -31,10 +32,12 @@ export default class Joplin {
private views_;
private interop_;
private settings_;
private contentScripts_;
constructor(implementation: any, plugin: Plugin, store: any);
get data(): JoplinData;
get plugins(): JoplinPlugins;
get workspace(): JoplinWorkspace;
get contentScripts(): JoplinContentScripts;
/**
* @ignore
*

View File

@@ -0,0 +1,40 @@
import Plugin from '../Plugin';
import { ContentScriptType } from './types';
export default class JoplinContentScripts {
private plugin;
constructor(plugin: Plugin);
/**
* Registers a new content script. Unlike regular plugin code, which runs in
* a separate process, content scripts run within the main process code and
* thus allow improved performances and more customisations in specific
* cases. It can be used for example to load a Markdown or editor plugin.
*
* Note that registering a content script in itself will do nothing - it
* will only be loaded in specific cases by the relevant app modules (eg.
* the Markdown renderer or the code editor). So it is not a way to inject
* and run arbitrary code in the app, which for safety and performance
* reasons is not supported.
*
* The plugin generator provides a way to build any content script you might
* want to package as well as its dependencies. See the [Plugin Generator
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
* for more information.
*
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
*
* See also the [postMessage demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
*
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
* @param id A unique ID for the content script.
* @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`.
*/
register(type: ContentScriptType, id: string, scriptPath: string): Promise<void>;
/**
* Listens to a messages sent from the content script using postMessage().
* See {@link ContentScriptType} for more information as well as the
* [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages)
*/
onMessage(contentScriptId: string, callback: any): Promise<void>;
}

View File

@@ -20,28 +20,7 @@ export default class JoplinPlugins {
*/
register(script: Script): Promise<void>;
/**
* Registers a new content script. Unlike regular plugin code, which runs in
* a separate process, content scripts run within the main process code and
* thus allow improved performances and more customisations in specific
* cases. It can be used for example to load a Markdown or editor plugin.
*
* Note that registering a content script in itself will do nothing - it
* will only be loaded in specific cases by the relevant app modules (eg.
* the Markdown renderer or the code editor). So it is not a way to inject
* and run arbitrary code in the app, which for safety and performance
* reasons is not supported.
*
* The plugin generator provides a way to build any content script you might
* want to package as well as its dependencies. See the [Plugin Generator
* doc](https://github.com/laurent22/joplin/blob/dev/packages/generator-joplin/README.md)
* for more information.
*
* * [View the renderer demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* * [View the editor demo plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
*
* @param type Defines how the script will be used. See the type definition for more information about each supported type.
* @param id A unique ID for the content script.
* @param scriptPath Must be a path relative to the plugin main script. For example, if your file content_script.js is next to your index.ts file, you would set `scriptPath` to `"./content_script.js`.
* @deprecated Use joplin.contentScripts.register()
*/
registerContentScript(type: ContentScriptType, id: string, scriptPath: string): Promise<void>;
}

View File

@@ -1,5 +1,12 @@
import Plugin from '../Plugin';
import { SettingItem, SettingSection } from './types';
export interface ChangeEvent {
/**
* Setting keys that have been changed
*/
keys: string[];
}
export declare type ChangeHandler = (event: ChangeEvent)=> void;
/**
* This API allows registering new settings and setting sections, as well as getting and setting settings. Once a setting has been registered it will appear in the config screen and be editable by the user.
*
@@ -12,6 +19,7 @@ import { SettingItem, SettingSection } from './types';
export default class JoplinSettings {
private plugin_;
constructor(plugin: Plugin);
private get keyPrefix();
private namespacedKey;
/**
* Registers a new setting. Note that registering a setting item is dynamic and will be gone next time Joplin starts.
@@ -40,4 +48,10 @@ export default class JoplinSettings {
* https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L142
*/
globalValue(key: string): Promise<any>;
/**
* Called when one or multiple settings of your plugin have been changed.
* - For performance reasons, this event is triggered with a delay.
* - You will only get events for your own plugin settings.
*/
onChange(handler: ChangeHandler): Promise<void>;
}

View File

@@ -28,6 +28,22 @@ export default class JoplinViewsPanels {
addScript(handle: ViewHandle, scriptPath: string): Promise<void>;
/**
* Called when a message is sent from the webview (using postMessage).
*
* To post a message from the webview to the plugin use:
*
* ```javascript
* const response = await webviewApi.postMessage(message);
* ```
*
* - `message` can be any JavaScript object, string or number
* - `response` is whatever was returned by the `onMessage` handler
*
* Using this mechanism, you can have two-way communication between the
* plugin and webview.
*
* See the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages) for more details.
*
*/
onMessage(handle: ViewHandle, callback: Function): Promise<void>;
/**

View File

@@ -366,9 +366,31 @@ export interface SettingSection {
export type Path = string[];
// =================================================================
// Plugins type
// Content Script types
// =================================================================
export type PostMessageHandler = (id: string, message: any)=> Promise<any>;
/**
* When a content script is initialised, it receives a `context` object.
*/
export interface ContentScriptContext {
/**
* The plugin ID that registered this content script
*/
pluginId: string;
/**
* The content script ID, which may be necessary to post messages
*/
contentScriptId: string;
/**
* Can be used by CodeMirror content scripts to post a message to the plugin
*/
postMessage: PostMessageHandler;
}
export enum ContentScriptType {
/**
* Registers a new Markdown-It plugin, which should follow the template
@@ -394,43 +416,56 @@ export enum ContentScriptType {
*
* ## Exported members
*
* - The `context` argument is currently unused but could be used later
* on to provide access to your own plugin so that the content script
* and plugin can communicate.
* - The `context` argument is currently unused but could be used later on
* to provide access to your own plugin so that the content script and
* plugin can communicate.
*
* - The **required** `plugin` key is the actual Markdown-It plugin -
* check the [official
* doc](https://github.com/markdown-it/markdown-it) for more
* - The **required** `plugin` key is the actual Markdown-It plugin - check
* the [official doc](https://github.com/markdown-it/markdown-it) for more
* information. The `options` parameter is of type
* [RuleOptions](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml.ts),
* which contains a number of options, mostly useful for Joplin's
* internal code.
* which contains a number of options, mostly useful for Joplin's internal
* code.
*
* - Using the **optional** `assets` key you may specify assets such as
* JS or CSS that should be loaded in the rendered HTML document.
* Check for example the Joplin [Mermaid
* - Using the **optional** `assets` key you may specify assets such as JS
* or CSS that should be loaded in the rendered HTML document. Check for
* example the Joplin [Mermaid
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured.
*
* ## Passing messages from the content script to your plugin
* ## Posting messages from the content script to your plugin
*
* The application provides the following function to allow executing
* commands from the rendered HTML code:
*
* `webviewApi.executeCommand(commandName, ...args)`
* ```javascript
* const response = await webviewApi.postMessage(contentScriptId, message);
* ```
*
* So you can use this mechanism to pass messages from the note viewer
* to your own plugin. To do so you would define a command, using
* `joplin.commands.register`, then you would call this command using
* the `webviewApi` object. See again [the
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/content_script)
* to see how this can be done.
* - `contentScriptId` is the ID you've defined when you registered the
* content script. You can retrieve it from the
* {@link ContentScriptContext | context}.
* - `message` can be any basic JavaScript type (number, string, plain
* object), but it cannot be a function or class instance.
*
* When you post a message, the plugin can send back a `response` thus
* allowing two-way communication:
*
* ```javascript
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
* // Process message
* return response; // Can be any object, string or number
* });
* ```
*
* See {@link JoplinContentScripts.onMessage} for more details, as well as
* the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
*
* ## Registering an existing Markdown-it plugin
*
* To include a regular Markdown-It plugin, that doesn't make use of
* any Joplin-specific features, you would simply create a file such as
* this:
* To include a regular Markdown-It plugin, that doesn't make use of any
* Joplin-specific features, you would simply create a file such as this:
*
* ```javascript
* module.exports = {
@@ -443,6 +478,7 @@ export enum ContentScriptType {
* ```
*/
MarkdownItPlugin = 'markdownItPlugin',
/**
* Registers a new CodeMirror plugin, which should follow the template
* below.
@@ -466,42 +502,65 @@ export enum ContentScriptType {
* }
* ```
*
* - The `context` argument is currently unused but could be used later
* on to provide access to your own plugin so that the content script
* and plugin can communicate.
* - The `context` argument is currently unused but could be used later on
* to provide access to your own plugin so that the content script and
* plugin can communicate.
*
* - The `plugin` key is your CodeMirror plugin. This is where you can
* register new commands with CodeMirror or interact with the
* CodeMirror instance as needed.
* register new commands with CodeMirror or interact with the CodeMirror
* instance as needed.
*
* - The `codeMirrorResources` key is an array of CodeMirror resources
* that will be loaded and attached to the CodeMirror module. These
* are made up of addons, keymaps, and modes. For example, for a
* plugin that want's to enable clojure highlighting in code blocks.
* `codeMirrorResources` would be set to `['mode/clojure/clojure']`.
* - The `codeMirrorResources` key is an array of CodeMirror resources that
* will be loaded and attached to the CodeMirror module. These are made up
* of addons, keymaps, and modes. For example, for a plugin that want's to
* enable clojure highlighting in code blocks. `codeMirrorResources` would
* be set to `['mode/clojure/clojure']`.
*
* - The `codeMirrorOptions` key contains all the
* [CodeMirror](https://codemirror.net/doc/manual.html#config)
* options that will be set or changed by this plugin. New options
* can alse be declared via
* [CodeMirror](https://codemirror.net/doc/manual.html#config) options
* that will be set or changed by this plugin. New options can alse be
* declared via
* [`CodeMirror.defineOption`](https://codemirror.net/doc/manual.html#defineOption),
* and then have their value set here. For example, a plugin that
* enables line numbers would set `codeMirrorOptions` to
* `{'lineNumbers': true}`.
* and then have their value set here. For example, a plugin that enables
* line numbers would set `codeMirrorOptions` to `{'lineNumbers': true}`.
*
* - Using the **optional** `assets` key you may specify **only** CSS
* assets that should be loaded in the rendered HTML document. Check
* for example the Joplin [Mermaid
* - Using the **optional** `assets` key you may specify **only** CSS assets
* that should be loaded in the rendered HTML document. Check for example
* the Joplin [Mermaid
* plugin](https://github.com/laurent22/joplin/blob/dev/packages/renderer/MdToHtml/rules/mermaid.ts)
* to see how the data should be structured.
*
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions`
* keys must be provided for the plugin to be valid. Having multiple or
* all provided is also okay.
* One of the `plugin`, `codeMirrorResources`, or `codeMirrorOptions` keys
* must be provided for the plugin to be valid. Having multiple or all
* provided is also okay.
*
* See the [demo
* See also the [demo
* plugin](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/codemirror_content_script)
* for an example of all these keys being used in one plugin.
*
* ## Posting messages from the content script to your plugin
*
* In order to post messages to the plugin, you can use the postMessage
* function passed to the {@link ContentScriptContext | context}.
*
* ```javascript
* const response = await context.postMessage('messageFromCodeMirrorContentScript');
* ```
*
* When you post a message, the plugin can send back a `response` thus
* allowing two-way communication:
*
* ```javascript
* await joplin.contentScripts.onMessage(contentScriptId, (message) => {
* // Process message
* return response; // Can be any object, string or number
* });
* ```
*
* See {@link JoplinContentScripts.onMessage} for more details, as well as
* the [postMessage
* demo](https://github.com/laurent22/joplin/tree/dev/packages/app-cli/tests/support/plugins/post_messages).
*
*/
CodeMirrorPlugin = 'codeMirrorPlugin',
}

View File

@@ -1,6 +1,6 @@
{
"name": "joplin-plugin-toggle-side-bars",
"version": "1.0.0",
"name": "@joplin/joplin-plugin-toggle-sidebars",
"version": "1.0.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1211,6 +1211,12 @@
"prr": "~1.0.1"
}
},
"escalade": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
"dev": true
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -4510,6 +4516,15 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
},
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
}
},
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
@@ -4536,6 +4551,40 @@
"json5": "^1.0.1"
}
},
"locate-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
}
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"dev": true
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
@@ -4544,6 +4593,24 @@
"requires": {
"has-flag": "^3.0.0"
}
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
}
}
}
},
@@ -4643,64 +4710,90 @@
"dev": true
},
"yargs": {
"version": "13.3.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
"integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
"version": "16.2.0",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"cliui": "^7.0.2",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.2"
"string-width": "^4.2.0",
"y18n": "^5.0.5",
"yargs-parser": "^20.2.2"
},
"dependencies": {
"find-up": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
"integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
"ansi-regex": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==",
"dev": true
},
"cliui": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
"dev": true,
"requires": {
"locate-path": "^3.0.0"
"string-width": "^4.2.0",
"strip-ansi": "^6.0.0",
"wrap-ansi": "^7.0.0"
}
},
"locate-path": {
"emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
"integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true
},
"string-width": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
"dev": true,
"requires": {
"p-locate": "^3.0.0",
"path-exists": "^3.0.0"
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.0"
}
},
"p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
"strip-ansi": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
"ansi-regex": "^5.0.0"
}
},
"p-locate": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
"integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
"wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"requires": {
"p-limit": "^2.0.0"
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
},
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
"y18n": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==",
"dev": true
},
"yargs-parser": {
"version": "20.2.4",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
"integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
"dev": true
}
}

View File

@@ -1,9 +1,10 @@
{
"name": "@joplin/joplin-plugin-toggle-sidebars",
"version": "1.0.2",
"version": "1.0.3",
"scripts": {
"dist": "webpack",
"prepare": "npm run dist"
"dist": "webpack --joplin-plugin-config buildMain && webpack --joplin-plugin-config buildExtraScripts && webpack --joplin-plugin-config createArchive",
"prepare": "npm run dist",
"update": "npm install -g generator-joplin && yo joplin --update"
},
"publishConfig": {
"access": "public"
@@ -23,6 +24,7 @@
"ts-loader": "^7.0.5",
"typescript": "^3.9.3",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.11"
"webpack-cli": "^3.3.11",
"yargs": "^16.2.0"
}
}

View File

@@ -0,0 +1,3 @@
{
"extraScripts": []
}

View File

@@ -2,8 +2,8 @@
"manifest_version": 1,
"id": "org.joplinapp.plugins.ToggleSidebars",
"app_min_version": "1.6",
"version": "1.0.2",
"name": "Note list and side bar toggle buttons",
"version": "1.0.3",
"name": "Note list and sidebar toggle buttons",
"description": "Adds buttons to toggle note list and sidebar",
"author": "Laurent Cozic",
"homepage_url": "https://github.com/laurent22/joplin/tree/dev/packages/plugins/ToggleSidebars",

View File

@@ -1,3 +1,11 @@
// -----------------------------------------------------------------------------
// This file is used to build the plugin file (.jpl) and plugin info (.json). It
// is recommended not to edit this file as it would be overwritten when updating
// the plugin framework. If you do make some changes, consider using an external
// JS file and requiring it here to minimize the changes. That way when you
// update, you can easily restore the functionality you've added.
// -----------------------------------------------------------------------------
const path = require('path');
const crypto = require('crypto');
const fs = require('fs-extra');
@@ -9,20 +17,22 @@ const glob = require('glob');
const execSync = require('child_process').execSync;
const rootDir = path.resolve(__dirname);
const userConfigFilename = './plugin.config.json';
const userConfigPath = path.resolve(rootDir, userConfigFilename);
const distDir = path.resolve(rootDir, 'dist');
const srcDir = path.resolve(rootDir, 'src');
const publishDir = path.resolve(rootDir, 'publish');
const userConfig = Object.assign({}, {
extraScripts: [],
}, fs.pathExistsSync(userConfigPath) ? require(userConfigFilename) : {});
const manifestPath = `${srcDir}/manifest.json`;
const packageJsonPath = `${rootDir}/package.json`;
const manifest = readManifest(manifestPath);
const pluginArchiveFilePath = path.resolve(publishDir, `${manifest.id}.jpl`);
const pluginInfoFilePath = path.resolve(publishDir, `${manifest.id}.json`);
fs.removeSync(distDir);
fs.removeSync(publishDir);
fs.mkdirpSync(publishDir);
function validatePackageJson() {
const content = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
if (!content.name || content.name.indexOf('joplin-plugin-') !== 0) {
@@ -68,13 +78,7 @@ function createPluginArchive(sourceDir, destPath) {
const distFiles = glob.sync(`${sourceDir}/**/*`, { nodir: true })
.map(f => f.substr(sourceDir.length + 1));
if (!distFiles.length) {
// Usually means there's an error, which is going to be printed by
// webpack
console.warn(chalk.yellow('Plugin archive was not created because the "dist" directory is empty'));
return;
}
if (!distFiles.length) throw new Error('Plugin archive was not created because the "dist" directory is empty');
fs.removeSync(destPath);
tar.create(
@@ -100,9 +104,14 @@ function createPluginInfo(manifestPath, destPath, jplFilePath) {
}
function onBuildCompleted() {
createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson();
try {
fs.removeSync(path.resolve(publishDir, 'index.js'));
createPluginArchive(distDir, pluginArchiveFilePath);
createPluginInfo(manifestPath, pluginInfoFilePath, pluginArchiveFilePath);
validatePackageJson();
} catch (error) {
console.error(chalk.red(error.message));
}
}
const baseConfig = {
@@ -132,9 +141,6 @@ const pluginConfig = Object.assign({}, baseConfig, {
filename: 'index.js',
path: distDir,
},
});
const lastStepConfig = {
plugins: [
new CopyPlugin({
patterns: [
@@ -148,23 +154,15 @@ const lastStepConfig = {
// already copied into /dist so we don't copy them.
'**/*.ts',
'**/*.tsx',
// Currently we don't support JS files for the main
// plugin script. We support it for content scripts,
// but they should be declared in manifest.json,
// and then they are also compiled and copied to
// /dist. So wse also don't need to copy JS files.
'**/*.js',
],
},
},
],
}),
new WebpackOnBuildPlugin(onBuildCompleted),
],
};
});
const contentScriptConfig = Object.assign({}, baseConfig, {
const extraScriptConfig = Object.assign({}, baseConfig, {
resolve: {
alias: {
api: path.resolve(__dirname, 'api'),
@@ -173,42 +171,46 @@ const contentScriptConfig = Object.assign({}, baseConfig, {
},
});
function resolveContentScriptPaths(name) {
if (['.js', '.ts', '.tsx'].includes(path.extname(name).toLowerCase())) {
throw new Error(`Content script path must not include file extension: ${name}`);
}
const createArchiveConfig = {
stats: 'errors-only',
entry: './dist/index.js',
output: {
filename: 'index.js',
path: publishDir,
},
plugins: [new WebpackOnBuildPlugin(onBuildCompleted)],
};
const pathsToTry = [
`./src/${name}.ts`,
`${'./src/' + '/'}${name}.js`,
];
function resolveExtraScriptPath(name) {
const relativePath = `./src/${name}`;
for (const pathToTry of pathsToTry) {
if (fs.pathExistsSync(`${rootDir}/${pathToTry}`)) {
return {
entry: pathToTry,
output: {
filename: `${name}.js`,
path: distDir,
library: 'default',
libraryTarget: 'commonjs',
libraryExport: 'default',
},
};
}
}
const fullPath = path.resolve(`${rootDir}/${relativePath}`);
if (!fs.pathExistsSync(fullPath)) throw new Error(`Could not find extra script: "${name}" at "${fullPath}"`);
throw new Error(`Could not find content script "${name}" at locations ${JSON.stringify(pathsToTry)}`);
const s = name.split('.');
s.pop();
const nameNoExt = s.join('.');
return {
entry: relativePath,
output: {
filename: `${nameNoExt}.js`,
path: distDir,
library: 'default',
libraryTarget: 'commonjs',
libraryExport: 'default',
},
};
}
function createContentScriptConfigs() {
if (!manifest.content_scripts) return [];
function buildExtraScriptConfigs(userConfig) {
if (!userConfig.extraScripts.length) return [];
const output = [];
for (const contentScriptName of manifest.content_scripts) {
const scriptPaths = resolveContentScriptPaths(contentScriptName);
output.push(Object.assign({}, contentScriptConfig, {
for (const scriptName of userConfig.extraScripts) {
const scriptPaths = resolveExtraScriptPath(scriptName);
output.push(Object.assign({}, extraScriptConfig, {
entry: scriptPaths.entry,
output: scriptPaths.output,
}));
@@ -217,8 +219,61 @@ function createContentScriptConfigs() {
return output;
}
const exportedConfigs = [pluginConfig].concat(createContentScriptConfigs());
function main(processArgv) {
const yargs = require('yargs/yargs');
const argv = yargs(processArgv).argv;
exportedConfigs[exportedConfigs.length - 1] = Object.assign({}, exportedConfigs[exportedConfigs.length - 1], lastStepConfig);
const configName = argv['joplin-plugin-config'];
if (!configName) throw new Error('A config file must be specified via the --joplin-plugin-config flag');
// Webpack configurations run in parallel, while we need them to run in
// sequence, and to do that it seems the only way is to run webpack multiple
// times, with different config each time.
const configs = {
// Builds the main src/index.ts and copy the extra content from /src to
// /dist including scripts, CSS and any other asset.
buildMain: [pluginConfig],
// Builds the extra scripts as defined in plugin.config.json. When doing
// so, some JavaScript files that were copied in the previous might be
// overwritten here by the compiled version. This is by design. The
// result is that JS files that don't need compilation, are simply
// copied to /dist, while those that do need it are correctly compiled.
buildExtraScripts: buildExtraScriptConfigs(userConfig),
// Ths config is for creating the .jpl, which is done via the plugin, so
// it doesn't actually need an entry and output, however webpack won't
// run without this. So we give it an entry that we know is going to
// exist and output in the publish dir. Then the plugin will delete this
// temporary file before packaging the plugin.
createArchive: [createArchiveConfig],
};
// If we are running the first config step, we clean up and create the build
// directories.
if (configName === 'buildMain') {
fs.removeSync(distDir);
fs.removeSync(publishDir);
fs.mkdirpSync(publishDir);
}
return configs[configName];
}
let exportedConfigs = [];
try {
exportedConfigs = main(process.argv);
} catch (error) {
console.error(chalk.red(error.message));
process.exit(1);
}
if (!exportedConfigs.length) {
// Nothing to do - for example where there are no external scripts to
// compile.
process.exit(0);
}
module.exports = exportedConfigs;

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