1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-12-23 23:33:01 +02:00

Compare commits

..

5 Commits

Author SHA1 Message Date
Laurent Cozic
9a536d9b5e bug 2021-11-01 16:32:56 +00:00
Laurent Cozic
0929699d7d update 2021-11-01 13:18:06 +00:00
Laurent Cozic
cc9cdd0bec tests 2021-11-01 09:23:05 +00:00
Laurent Cozic
08050f6d28 sqlite 2021-11-01 08:50:37 +00:00
Laurent Cozic
63cbdd9a62 native locks 2021-11-01 08:24:51 +00:00
42 changed files with 9746 additions and 2175 deletions

View File

@@ -28,7 +28,6 @@ build/
.gradle
local.properties
*.iml
*.hprof
# node.js
#

View File

@@ -123,11 +123,6 @@ def jscFlavor = 'org.webkit:android-jsc-intl:+'
*/
def enableHermes = project.ext.react.get("enableHermes", false);
/**
* Architectures to build native code for in debug.
*/
def nativeArchitectures = project.getProperties().get("reactNativeDebugArchitectures")
android {
compileSdkVersion rootProject.ext.compileSdkVersion
@@ -185,11 +180,6 @@ android {
buildTypes {
debug {
signingConfig signingConfigs.debug
if (nativeArchitectures) {
ndk {
abiFilters nativeArchitectures.split(',')
}
}
}
release {
// Caution! In production, you need to generate your own keystore file.
@@ -225,7 +215,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}") {
exclude group:'com.facebook.fbjni'
exclude group:'com.facebook.fbjni'
}
debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
@@ -252,7 +242,7 @@ dependencies {
// Run this once to be able to run the application with BUCK
// puts all compile dependencies into folder libs for BUCK to use
task copyDownloadableDepsToLibs(type: Copy) {
from configurations.implementation
from configurations.compile
into 'libs'
}

View File

@@ -3,6 +3,7 @@
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="android:textColor">#000000</item>
</style>
</resources>

View File

@@ -2,18 +2,18 @@
buildscript {
ext {
buildToolsVersion = "30.0.2"
buildToolsVersion = "29.0.3"
minSdkVersion = 21
compileSdkVersion = 30
targetSdkVersion = 30
ndkVersion = "21.4.7075529"
compileSdkVersion = 29
targetSdkVersion = 29
ndkVersion = "20.1.5948944"
}
repositories {
google()
mavenCentral()
jcenter()
}
dependencies {
classpath("com.android.tools.build:gradle:4.2.2")
classpath("com.android.tools.build:gradle:4.1.0")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
@@ -21,7 +21,6 @@ buildscript {
allprojects {
repositories {
mavenCentral()
mavenLocal()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
@@ -33,6 +32,7 @@ allprojects {
}
google()
jcenter()
maven { url 'https://www.jitpack.io' }
}
}

View File

@@ -26,4 +26,4 @@ android.useAndroidX=true
android.enableJetifier=true
# Version of flipper SDK to use with React Native
FLIPPER_VERSION=0.99.0
FLIPPER_VERSION=0.75.1

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -3,7 +3,7 @@ require_relative '../node_modules/@react-native-community/cli-platform-ios/nativ
# Note: it was 13.4 to get @react-native-community/datetimepicker to work
# but it's probably not necessary actually. Just needed to upgrade XCode.
platform :ios, '11.0'
platform :ios, '10.0'
target 'Joplin' do
config = use_native_modules!
@@ -24,7 +24,6 @@ target 'Joplin' do
# use_flipper!()
# post_install do |installer|
# react_native_post_install(installer)
# __apply_Xcode_12_5_M1_post_install_workaround(installer)
# end
# RN 0.63:

View File

@@ -1,218 +1,212 @@
PODS:
- boost (1.76.0)
- boost-for-react-native (1.63.0)
- DoubleConversion (1.1.6)
- FBLazyVector (0.66.1)
- FBReactNativeSpec (0.66.1):
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.66.1)
- RCTTypeSafety (= 0.66.1)
- React-Core (= 0.66.1)
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- fmt (6.2.1)
- FBLazyVector (0.64.2)
- FBReactNativeSpec (0.64.2):
- RCT-Folly (= 2020.01.13.00)
- RCTRequired (= 0.64.2)
- RCTTypeSafety (= 0.64.2)
- React-Core (= 0.64.2)
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- glog (0.3.5)
- JoplinCommonShareExtension (1.0.0)
- JoplinRNShareExtension (1.0.0):
- JoplinCommonShareExtension
- React (= 0.66.1)
- RCT-Folly (2021.06.28.00-v2):
- boost
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCT-Folly/Default (= 2021.06.28.00-v2)
- RCT-Folly/Default (2021.06.28.00-v2):
- boost
- DoubleConversion
- fmt (~> 6.2.1)
- glog
- RCTRequired (0.66.1)
- RCTTypeSafety (0.66.1):
- FBLazyVector (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTRequired (= 0.66.1)
- React-Core (= 0.66.1)
- React (0.66.1):
- React-Core (= 0.66.1)
- React-Core/DevSupport (= 0.66.1)
- React-Core/RCTWebSocket (= 0.66.1)
- React-RCTActionSheet (= 0.66.1)
- React-RCTAnimation (= 0.66.1)
- React-RCTBlob (= 0.66.1)
- React-RCTImage (= 0.66.1)
- React-RCTLinking (= 0.66.1)
- React-RCTNetwork (= 0.66.1)
- React-RCTSettings (= 0.66.1)
- React-RCTText (= 0.66.1)
- React-RCTVibration (= 0.66.1)
- React-callinvoker (0.66.1)
- React-Core (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.66.1)
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/CoreModulesHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/Default (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/DevSupport (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.66.1)
- React-Core/RCTWebSocket (= 0.66.1)
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-jsinspector (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTActionSheetHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTAnimationHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTBlobHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTImageHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTLinkingHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTNetworkHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTSettingsHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTTextHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTVibrationHeaders (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-Core/RCTWebSocket (0.66.1):
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/Default (= 0.66.1)
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsiexecutor (= 0.66.1)
- React-perflogger (= 0.66.1)
- Yoga
- React-CoreModules (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.66.1)
- React-Core/CoreModulesHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- React-RCTImage (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-cxxreact (0.66.1):
- boost (= 1.76.0)
- React (= 0.64.2)
- RCT-Folly (2020.01.13.00):
- boost-for-react-native
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.66.1)
- React-jsi (= 0.66.1)
- React-jsinspector (= 0.66.1)
- React-logger (= 0.66.1)
- React-perflogger (= 0.66.1)
- React-runtimeexecutor (= 0.66.1)
- React-jsi (0.66.1):
- boost (= 1.76.0)
- RCT-Folly/Default (= 2020.01.13.00)
- RCT-Folly/Default (2020.01.13.00):
- boost-for-react-native
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsi/Default (= 0.66.1)
- React-jsi/Default (0.66.1):
- boost (= 1.76.0)
- RCTRequired (0.64.2)
- RCTTypeSafety (0.64.2):
- FBLazyVector (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- RCTRequired (= 0.64.2)
- React-Core (= 0.64.2)
- React (0.64.2):
- React-Core (= 0.64.2)
- React-Core/DevSupport (= 0.64.2)
- React-Core/RCTWebSocket (= 0.64.2)
- React-RCTActionSheet (= 0.64.2)
- React-RCTAnimation (= 0.64.2)
- React-RCTBlob (= 0.64.2)
- React-RCTImage (= 0.64.2)
- React-RCTLinking (= 0.64.2)
- React-RCTNetwork (= 0.64.2)
- React-RCTSettings (= 0.64.2)
- React-RCTText (= 0.64.2)
- React-RCTVibration (= 0.64.2)
- React-callinvoker (0.64.2)
- React-Core (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default (= 0.64.2)
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/CoreModulesHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/Default (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/DevSupport (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default (= 0.64.2)
- React-Core/RCTWebSocket (= 0.64.2)
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-jsinspector (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTActionSheetHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTAnimationHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTBlobHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTImageHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTLinkingHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTNetworkHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTSettingsHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTTextHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTVibrationHeaders (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-Core/RCTWebSocket (0.64.2):
- glog
- RCT-Folly (= 2020.01.13.00)
- React-Core/Default (= 0.64.2)
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsiexecutor (= 0.64.2)
- React-perflogger (= 0.64.2)
- Yoga
- React-CoreModules (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.64.2)
- React-Core/CoreModulesHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- React-RCTImage (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-cxxreact (0.64.2):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-jsiexecutor (0.66.1):
- RCT-Folly (= 2020.01.13.00)
- React-callinvoker (= 0.64.2)
- React-jsi (= 0.64.2)
- React-jsinspector (= 0.64.2)
- React-perflogger (= 0.64.2)
- React-runtimeexecutor (= 0.64.2)
- React-jsi (0.64.2):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-perflogger (= 0.66.1)
- React-jsinspector (0.66.1)
- React-logger (0.66.1):
- RCT-Folly (= 2020.01.13.00)
- React-jsi/Default (= 0.64.2)
- React-jsi/Default (0.64.2):
- boost-for-react-native (= 1.63.0)
- DoubleConversion
- glog
- RCT-Folly (= 2020.01.13.00)
- React-jsiexecutor (0.64.2):
- DoubleConversion
- glog
- RCT-Folly (= 2020.01.13.00)
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-perflogger (= 0.64.2)
- React-jsinspector (0.64.2)
- react-native-alarm-notification (1.0.3):
- React
- react-native-camera (3.40.0):
@@ -243,71 +237,70 @@ PODS:
- React-Core
- react-native-webview (10.9.2):
- React-Core
- React-perflogger (0.66.1)
- React-RCTActionSheet (0.66.1):
- React-Core/RCTActionSheetHeaders (= 0.66.1)
- React-RCTAnimation (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.66.1)
- React-Core/RCTAnimationHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-RCTBlob (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/RCTBlobHeaders (= 0.66.1)
- React-Core/RCTWebSocket (= 0.66.1)
- React-jsi (= 0.66.1)
- React-RCTNetwork (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-RCTImage (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.66.1)
- React-Core/RCTImageHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- React-RCTNetwork (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-RCTLinking (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- React-Core/RCTLinkingHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-RCTNetwork (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.66.1)
- React-Core/RCTNetworkHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-RCTSettings (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- RCTTypeSafety (= 0.66.1)
- React-Core/RCTSettingsHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-RCTText (0.66.1):
- React-Core/RCTTextHeaders (= 0.66.1)
- React-RCTVibration (0.66.1):
- FBReactNativeSpec (= 0.66.1)
- RCT-Folly (= 2021.06.28.00-v2)
- React-Core/RCTVibrationHeaders (= 0.66.1)
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (= 0.66.1)
- React-runtimeexecutor (0.66.1):
- React-jsi (= 0.66.1)
- ReactCommon/turbomodule/core (0.66.1):
- React-perflogger (0.64.2)
- React-RCTActionSheet (0.64.2):
- React-Core/RCTActionSheetHeaders (= 0.64.2)
- React-RCTAnimation (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.64.2)
- React-Core/RCTAnimationHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-RCTBlob (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- React-Core/RCTBlobHeaders (= 0.64.2)
- React-Core/RCTWebSocket (= 0.64.2)
- React-jsi (= 0.64.2)
- React-RCTNetwork (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-RCTImage (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.64.2)
- React-Core/RCTImageHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- React-RCTNetwork (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-RCTLinking (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- React-Core/RCTLinkingHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-RCTNetwork (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.64.2)
- React-Core/RCTNetworkHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-RCTSettings (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- RCTTypeSafety (= 0.64.2)
- React-Core/RCTSettingsHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-RCTText (0.64.2):
- React-Core/RCTTextHeaders (= 0.64.2)
- React-RCTVibration (0.64.2):
- FBReactNativeSpec (= 0.64.2)
- RCT-Folly (= 2020.01.13.00)
- React-Core/RCTVibrationHeaders (= 0.64.2)
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (= 0.64.2)
- React-runtimeexecutor (0.64.2):
- React-jsi (= 0.64.2)
- ReactCommon/turbomodule/core (0.64.2):
- DoubleConversion
- glog
- RCT-Folly (= 2021.06.28.00-v2)
- React-callinvoker (= 0.66.1)
- React-Core (= 0.66.1)
- React-cxxreact (= 0.66.1)
- React-jsi (= 0.66.1)
- React-logger (= 0.66.1)
- React-perflogger (= 0.66.1)
- RCT-Folly (= 2020.01.13.00)
- React-callinvoker (= 0.64.2)
- React-Core (= 0.64.2)
- React-cxxreact (= 0.64.2)
- React-jsi (= 0.64.2)
- React-perflogger (= 0.64.2)
- rn-fetch-blob (0.12.0):
- React-Core
- RNCClipboard (1.5.0):
@@ -331,7 +324,6 @@ PODS:
- Yoga (1.14.0)
DEPENDENCIES:
- boost (from `../node_modules/react-native/third-party-podspecs/boost.podspec`)
- DoubleConversion (from `../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
@@ -351,7 +343,6 @@ DEPENDENCIES:
- React-jsi (from `../node_modules/react-native/ReactCommon/jsi`)
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
- react-native-alarm-notification (from `../node_modules/joplin-rn-alarm-notification`)
- react-native-camera (from `../node_modules/react-native-camera`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
@@ -390,11 +381,9 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- fmt
- boost-for-react-native
EXTERNAL SOURCES:
boost:
:podspec: "../node_modules/react-native/third-party-podspecs/boost.podspec"
DoubleConversion:
:podspec: "../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec"
FBLazyVector:
@@ -429,8 +418,6 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/jsiexecutor"
React-jsinspector:
:path: "../node_modules/react-native/ReactCommon/jsinspector"
React-logger:
:path: "../node_modules/react-native/ReactCommon/logger"
react-native-alarm-notification:
:path: "../node_modules/joplin-rn-alarm-notification"
react-native-camera:
@@ -503,26 +490,24 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native/ReactCommon/yoga"
SPEC CHECKSUMS:
boost: a7c83b31436843459a1961bfd74b96033dc77234
DoubleConversion: 831926d9b8bf8166fd87886c4abab286c2422662
FBLazyVector: 500821d196c3d1bd10e7e828bc93ce075234080f
FBReactNativeSpec: 74c869e2cffa2ffec685cd1bac6788c021da6005
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
glog: 5337263514dd6f09803962437687240c5dc39aa4
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: 009b310a5134a345e702b4402de70b5ee2bb4832
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
JoplinCommonShareExtension: 270b4f8eb4e22828eeda433a04ed689fc1fd09b5
JoplinRNShareExtension: cb790ce4c0692367acd1a06c56330c9a440f8b58
RCT-Folly: a21c126816d8025b547704b777a2ba552f3d9fa9
RCTRequired: 3cc065b52aa18db729268b9bd78a2feffb4d0f91
RCTTypeSafety: 3c4fc37d5dea452d2ef17324db5504ec2f05083a
React: 4a00720816c52a213424442954acb7e4b724804a
React-callinvoker: 911fc6570538f3bb5c61edf9dc907c1beb4355bf
React-Core: e134d3a5d7b2a1a731589be776e20dbb14868f27
React-CoreModules: 2f8588b2aa47e7fef27125c8eaaabda963b3ac62
React-cxxreact: 8f1382538cad0cc8b8eafca6d66268828e353bea
React-jsi: 9fe1854d2c0486216acebd5db3c38b4ccb23ca0b
React-jsiexecutor: db2f6e22a534d466fc0e34e622df47d9d20bab2f
React-jsinspector: 8c0517dee5e8c70cd6c3066f20213ff7ce54f176
React-logger: bfddd3418dc1d45b77b822958f3e31422e2c179b
JoplinRNShareExtension: 7137e9787374e1b0797ecbef9103d1588d90e403
RCT-Folly: ec7a233ccc97cc556cf7237f0db1ff65b986f27c
RCTRequired: 6d3e854f0e7260a648badd0d44fc364bc9da9728
RCTTypeSafety: c1f31d19349c6b53085766359caac425926fafaa
React: bda6b6d7ae912de97d7a61aa5c160db24aa2ad69
React-callinvoker: 9840ea7e8e88ed73d438edb725574820b29b5baa
React-Core: b5e385da7ce5f16a220fc60fd0749eae2c6120f0
React-CoreModules: 17071a4e2c5239b01585f4aa8070141168ab298f
React-cxxreact: 9be7b6340ed9f7c53e53deca7779f07cd66525ba
React-jsi: 67747b9722f6dab2ffe15b011bcf6b3f2c3f1427
React-jsiexecutor: 80c46bd381fd06e418e0d4f53672dc1d1945c4c3
React-jsinspector: cc614ec18a9ca96fd275100c16d74d62ee11f0ae
react-native-alarm-notification: 466e4ad56fbd948ecac26e657f292dca8bf483d5
react-native-camera: 35854c4f764a4a6cf61c1c3525888b92f0fe4b31
react-native-document-picker: 0bba80cc56caab1f67dbaa81ff557e3a9b7f2b9f
@@ -535,18 +520,18 @@ SPEC CHECKSUMS:
react-native-sqlite-storage: 418ef4afc5e6df6ce3574c4617e5f0b65cffde55
react-native-version-info: 36490da17d2c6b5cc21321c70e433784dee7ed0b
react-native-webview: 4e96d493f9f90ba4f03b28933f30b2964df07e39
React-perflogger: fcac6090a80e3d967791b4c7f1b1a017f9d4a398
React-RCTActionSheet: caf5913d9f9e605f5467206cf9d1caa6d47d7ad6
React-RCTAnimation: 6539e3bf594f6a529cd861985ba6548286ae1ead
React-RCTBlob: 6e2e999d28b15fd03ed533f164ce33e0fcde571a
React-RCTImage: c6bbb10eedb6b840c4474f2108b864173b83de15
React-RCTLinking: 8fda9bb8fdb104e78110a903a9a77754318c7d11
React-RCTNetwork: 2b26daad93830501cf14aab03eac04e304f942d3
React-RCTSettings: 89c0dcee7adb706c749383596f57c1e882a27843
React-RCTText: 71734fce8e6cb854daeb4a5eec182c303ea58473
React-RCTVibration: 6600b5eed7c0fda4a433fa1198d1cb2690151791
React-runtimeexecutor: 33a949a51bec5f8a3c9e8d8092deb259600d761e
ReactCommon: 620442811dc6f707b4bf5e3b27d4f19c12d5a821
React-perflogger: 25373e382fed75ce768a443822f07098a15ab737
React-RCTActionSheet: af7796ba49ffe4ca92e7277a5d992d37203f7da5
React-RCTAnimation: 6a2e76ab50c6f25b428d81b76a5a45351c4d77aa
React-RCTBlob: 02a2887023e0eed99391b6445b2e23a2a6f9226d
React-RCTImage: ce5bf8e7438f2286d9b646a05d6ab11f38b0323d
React-RCTLinking: ccd20742de14e020cb5f99d5c7e0bf0383aefbd9
React-RCTNetwork: dfb9d089ab0753e5e5f55fc4b1210858f7245647
React-RCTSettings: b14aef2d83699e48b410fb7c3ba5b66cd3291ae2
React-RCTText: 41a2e952dd9adc5caf6fb68ed46b275194d5da5f
React-RCTVibration: 24600e3b1aaa77126989bc58b6747509a1ba14f3
React-runtimeexecutor: a9904c6d0218fb9f8b19d6dd88607225927668f9
ReactCommon: 149906e01aa51142707a10665185db879898e966
rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba
RNCClipboard: c7abea1baea58adca5c1f29e56dd5261837b4892
RNCPushNotificationIOS: ec7ffe65c7b5097f8d287fd627e1c1674ea69cef
@@ -557,8 +542,8 @@ SPEC CHECKSUMS:
RNSecureRandom: 1f19ad1492f7ed416b8fc79e92216a1f73f13a4c
RNShare: 9cdd23357981cf4dee275eb79239e860dccc0faf
RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59
Yoga: 2b4a01651f42a32f82e6cef3830a3ba48088237f
Yoga: 575c581c63e0d35c9a83f4b46d01d63abc1100ac
PODFILE CHECKSUM: 9f8b595b05d63f54759fc6d9b1d2c5838fff9626
PODFILE CHECKSUM: 6aeb91c381b1a03c0c8c78b2f504699bd569b7e3
COCOAPODS: 1.10.2

View File

@@ -9,6 +9,6 @@ Pod::Spec.new do |spec|
spec.platform = :ios, "9.0"
spec.source = { :path => "." }
spec.source_files = "Source/RNShareExtension/**/*.{h,m}"
spec.dependency "React", "0.66.1"
spec.dependency "React", "0.64.2"
spec.dependency "JoplinCommonShareExtension"
end

File diff suppressed because it is too large Load Diff

View File

@@ -33,8 +33,8 @@
"md5": "^2.2.1",
"prop-types": "^15.6.0",
"punycode": "^2.1.1",
"react": "17.0.2",
"react-native": "0.66.1",
"react": "17.0.1",
"react-native": "0.64.2",
"react-native-action-button": "^2.8.5",
"react-native-camera": "^3.40.0",
"react-native-dialogbox": "^0.6.10",
@@ -84,7 +84,7 @@
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",
"jetifier": "^1.6.5",
"metro-react-native-babel-preset": "^0.66.2",
"metro-react-native-babel-preset": "^0.64.0",
"nodemon": "^2.0.12",
"rollup": "^2.53.1",
"typescript": "^4.0.5",

View File

@@ -505,9 +505,9 @@ async function initialize(dispatch: Function) {
Setting.setValue('sync.10.path', 'http://api.joplincloud.local:22300');
Setting.setValue('sync.10.userContentPath', 'http://joplinusercontent.local:22300');
// Setting.setValue('sync.target', 10);
// Setting.setValue('sync.10.username', 'user1@example.com');
// Setting.setValue('sync.10.password', 'hunter1hunter2hunter3');
Setting.setValue('sync.target', 10);
Setting.setValue('sync.10.username', 'user1@example.com');
Setting.setValue('sync.10.password', 'hunter1hunter2hunter3');
}
if (Setting.value('db.ftsEnabled') === -1) {

View File

@@ -253,8 +253,12 @@ export default class JoplinServerApi {
const output = await loadResponseJson();
return output;
} catch (error) {
if (error.code !== 404) {
// Don't print error info for file not found (handled by the
// driver), or lock-acquisition errors because it's handled by
// LockHandler.
if (![404, 'hasExclusiveLock', 'hasSyncLock'].includes(error.code)) {
logger.warn(this.requestToCurl_(url, fetchOptions));
logger.warn('Code:', error.code);
logger.warn(error);
}

View File

@@ -1,5 +1,5 @@
import Logger from './Logger';
import LockHandler, { LockType } from './services/synchronizer/LockHandler';
import LockHandler, { hasActiveLock, LockType } from './services/synchronizer/LockHandler';
import Setting from './models/Setting';
import shim from './shim';
import MigrationHandler from './services/synchronizer/MigrationHandler';
@@ -298,10 +298,13 @@ export default class Synchronizer {
}
async lockErrorStatus_() {
const hasActiveExclusiveLock = await this.lockHandler().hasActiveLock(LockType.Exclusive);
const locks = await this.lockHandler().locks();
const currentDate = await this.lockHandler().currentDate();
const hasActiveExclusiveLock = await hasActiveLock(locks, currentDate, this.lockHandler().lockTtl, LockType.Exclusive);
if (hasActiveExclusiveLock) return 'hasExclusiveLock';
const hasActiveSyncLock = await this.lockHandler().hasActiveLock(LockType.Sync, this.appType_, this.clientId_);
const hasActiveSyncLock = await hasActiveLock(locks, currentDate, this.lockHandler().lockTtl, LockType.Sync, this.appType_, this.clientId_);
if (!hasActiveSyncLock) return 'syncLockGone';
return '';

View File

@@ -2,6 +2,7 @@ import { MultiPutItem } from './file-api';
import JoplinError from './JoplinError';
import JoplinServerApi from './JoplinServerApi';
import { trimSlashes } from './path-utils';
import { Lock, LockType } from './services/synchronizer/LockHandler';
// All input paths should be in the format: "path/to/file". This is converted to
// "root:/path/to/file:" when doing the API call.
@@ -40,6 +41,10 @@ export default class FileApiDriverJoplinServer {
return true;
}
public get supportsLocks() {
return true;
}
public requestRepeatCount() {
return 3;
}
@@ -196,6 +201,22 @@ export default class FileApiDriverJoplinServer {
throw new Error('Not supported');
}
public async acquireLock(type: LockType, clientType: string, clientId: string): Promise<Lock> {
return this.api().exec('POST', 'api/locks', null, {
type,
clientType: clientType,
clientId: clientId,
});
}
public async releaseLock(type: LockType, clientType: string, clientId: string) {
await this.api().exec('DELETE', `api/locks/${type}_${clientType}_${clientId}`);
}
public async listLocks() {
return this.api().exec('GET', 'api/locks');
}
public async clearRoot(path: string) {
const response = await this.list(path);
@@ -203,6 +224,8 @@ export default class FileApiDriverJoplinServer {
await this.delete(item.path);
}
await this.api().exec('POST', 'api/debug', null, { action: 'clearKeyValues' });
if (response.has_more) throw new Error('has_more support not implemented');
}
}

View File

@@ -5,6 +5,7 @@ import time from './time';
const { isHidden } = require('./path-utils');
import JoplinError from './JoplinError';
import { Lock, LockType } from './services/synchronizer/LockHandler';
const ArrayUtils = require('./ArrayUtils');
const { sprintf } = require('sprintf-js');
const Mutex = require('async-mutex').Mutex;
@@ -36,7 +37,7 @@ export interface RemoteItem {
export interface PaginatedList {
items: RemoteItem[];
has_more: boolean;
hasMore: boolean;
context: any;
}
@@ -130,6 +131,10 @@ class FileApi {
return !!this.driver().supportsAccurateTimestamp;
}
public get supportsLocks(): boolean {
return !!this.driver().supportsLocks;
}
async fetchRemoteDateOffset_() {
const tempFile = `${this.tempDirName()}/timeCheck${Math.round(Math.random() * 1000000)}.txt`;
const startTime = Date.now();
@@ -349,6 +354,22 @@ class FileApi {
logger.debug(`delta ${this.fullPath(path)}`);
return tryAndRepeat(() => this.driver_.delta(this.fullPath(path), options), this.requestRepeatCount());
}
public async acquireLock(type: LockType, clientType: string, clientId: string): Promise<Lock> {
if (!this.supportsLocks) throw new Error('Sync target does not support built-in locks');
return tryAndRepeat(() => this.driver_.acquireLock(type, clientType, clientId), this.requestRepeatCount());
}
public async releaseLock(type: LockType, clientType: string, clientId: string) {
if (!this.supportsLocks) throw new Error('Sync target does not support built-in locks');
return tryAndRepeat(() => this.driver_.releaseLock(type, clientType, clientId), this.requestRepeatCount());
}
public async listLocks() {
if (!this.supportsLocks) throw new Error('Sync target does not support built-in locks');
return tryAndRepeat(() => this.driver_.listLocks(), this.requestRepeatCount());
}
}
function basicDeltaContextFromOptions_(options: any) {

View File

@@ -3,6 +3,7 @@ import shim from '../../shim';
import JoplinError from '../../JoplinError';
import time from '../../time';
import { FileApi } from '../../file-api';
const { fileExtension, filename } = require('../../path-utils');
export enum LockType {
@@ -12,12 +13,68 @@ export enum LockType {
}
export interface Lock {
id?: string;
type: LockType;
clientType: string;
clientId: string;
updatedTime?: number;
}
function lockIsActive(lock: Lock, currentDate: Date, lockTtl: number): boolean {
return currentDate.getTime() - lock.updatedTime < lockTtl;
}
export function lockNameToObject(name: string, updatedTime: number = null): Lock {
const p = name.split('_');
return {
type: p[0] as LockType,
clientType: p[1],
clientId: p[2],
updatedTime: updatedTime,
};
}
export function hasActiveLock(locks: Lock[], currentDate: Date, lockTtl: number, lockType: LockType, clientType: string = null, clientId: string = null) {
const lock = activeLock(locks, currentDate, lockTtl, lockType, clientType, clientId);
return !!lock;
}
// Finds if there's an active lock for this clientType and clientId and returns it.
// If clientType and clientId are not specified, returns the first active lock
// of that type instead.
export function activeLock(locks: Lock[], currentDate: Date, lockTtl: number, lockType: LockType, clientType: string = null, clientId: string = null) {
if (lockType === LockType.Exclusive) {
const activeLocks = locks
.slice()
.filter((lock: Lock) => lockIsActive(lock, currentDate, lockTtl) && lock.type === lockType)
.sort((a: Lock, b: Lock) => {
if (a.updatedTime === b.updatedTime) {
return a.clientId < b.clientId ? -1 : +1;
}
return a.updatedTime < b.updatedTime ? -1 : +1;
});
if (!activeLocks.length) return null;
const lock = activeLocks[0];
if (clientType && clientType !== lock.clientType) return null;
if (clientId && clientId !== lock.clientId) return null;
return lock;
} else if (lockType === LockType.Sync) {
for (const lock of locks) {
if (lock.type !== lockType) continue;
if (clientType && lock.clientType !== clientType) continue;
if (clientId && lock.clientId !== clientId) continue;
if (lockIsActive(lock, currentDate, lockTtl)) return lock;
}
return null;
}
throw new Error(`Unsupported lock type: ${lockType}`);
}
export interface AcquireLockOptions {
// In theory, a client that tries to acquire an exclusive lock shouldn't
// also have a sync lock. It can however happen when the app is closed
@@ -55,14 +112,16 @@ export interface LockHandlerOptions {
lockTtl?: number;
}
export const lockDefaultTtl = 1000 * 60 * 3;
export default class LockHandler {
private api_: any = null;
private api_: FileApi = null;
private refreshTimers_: RefreshTimers = {};
private autoRefreshInterval_: number = 1000 * 60;
private lockTtl_: number = 1000 * 60 * 3;
private lockTtl_: number = lockDefaultTtl;
constructor(api: any, options: LockHandlerOptions = null) {
public constructor(api: FileApi, options: LockHandlerOptions = null) {
if (!options) options = {};
this.api_ = api;
@@ -80,6 +139,10 @@ export default class LockHandler {
this.lockTtl_ = v;
}
public get useBuiltInLocks() {
return this.api_.supportsLocks;
}
private lockFilename(lock: Lock) {
return `${[lock.type, lock.clientType, lock.clientId].join('_')}.json`;
}
@@ -97,17 +160,15 @@ export default class LockHandler {
}
private lockFileToObject(file: any): Lock {
const p = filename(file.path).split('_');
return {
type: p[0],
clientType: p[1],
clientId: p[2],
updatedTime: file.updated_time,
};
return lockNameToObject(filename(file.path), file.updated_time);
}
async locks(lockType: LockType = null): Promise<Lock[]> {
if (this.useBuiltInLocks) {
const locks = (await this.api_.listLocks()).items;
return locks;
}
const result = await this.api_.list(Dirnames.Locks);
if (result.hasMore) throw new Error('hasMore not handled'); // Shouldn't happen anyway
@@ -123,51 +184,6 @@ export default class LockHandler {
return output;
}
private lockIsActive(lock: Lock, currentDate: Date): boolean {
return currentDate.getTime() - lock.updatedTime < this.lockTtl;
}
async hasActiveLock(lockType: LockType, clientType: string = null, clientId: string = null) {
const lock = await this.activeLock(lockType, clientType, clientId);
return !!lock;
}
// Finds if there's an active lock for this clientType and clientId and returns it.
// If clientType and clientId are not specified, returns the first active lock
// of that type instead.
async activeLock(lockType: LockType, clientType: string = null, clientId: string = null) {
const locks = await this.locks(lockType);
const currentDate = await this.api_.remoteDate();
if (lockType === LockType.Exclusive) {
const activeLocks = locks
.slice()
.filter((lock: Lock) => this.lockIsActive(lock, currentDate))
.sort((a: Lock, b: Lock) => {
if (a.updatedTime === b.updatedTime) {
return a.clientId < b.clientId ? -1 : +1;
}
return a.updatedTime < b.updatedTime ? -1 : +1;
});
if (!activeLocks.length) return null;
const activeLock = activeLocks[0];
if (clientType && clientType !== activeLock.clientType) return null;
if (clientId && clientId !== activeLock.clientId) return null;
return activeLock;
} else if (lockType === LockType.Sync) {
for (const lock of locks) {
if (clientType && lock.clientType !== clientType) continue;
if (clientId && lock.clientId !== clientId) continue;
if (this.lockIsActive(lock, currentDate)) return lock;
}
return null;
}
throw new Error(`Unsupported lock type: ${lockType}`);
}
private async saveLock(lock: Lock) {
await this.api_.put(this.lockFilePath(lock), JSON.stringify(lock));
}
@@ -178,12 +194,17 @@ export default class LockHandler {
}
private async acquireSyncLock(clientType: string, clientId: string): Promise<Lock> {
if (this.useBuiltInLocks) return this.api_.acquireLock(LockType.Sync, clientType, clientId);
try {
let isFirstPass = true;
while (true) {
const locks = await this.locks();
const currentDate = await this.currentDate();
const [exclusiveLock, syncLock] = await Promise.all([
this.activeLock(LockType.Exclusive),
this.activeLock(LockType.Sync, clientType, clientId),
activeLock(locks, currentDate, this.lockTtl, LockType.Exclusive),
activeLock(locks, currentDate, this.lockTtl, LockType.Sync, clientType, clientId),
]);
if (exclusiveLock) {
@@ -222,6 +243,8 @@ export default class LockHandler {
}
private async acquireExclusiveLock(clientType: string, clientId: string, options: AcquireLockOptions = null): Promise<Lock> {
if (this.useBuiltInLocks) return this.api_.acquireLock(LockType.Exclusive, clientType, clientId);
// The logic to acquire an exclusive lock, while avoiding race conditions is as follow:
//
// - Check if there is a lock file present
@@ -252,9 +275,12 @@ export default class LockHandler {
try {
while (true) {
const locks = await this.locks();
const currentDate = await this.currentDate();
const [activeSyncLock, activeExclusiveLock] = await Promise.all([
this.activeLock(LockType.Sync),
this.activeLock(LockType.Exclusive),
activeLock(locks, currentDate, this.lockTtl, LockType.Sync),
activeLock(locks, currentDate, this.lockTtl, LockType.Exclusive),
]);
if (activeSyncLock) {
@@ -299,7 +325,11 @@ export default class LockHandler {
return [lock.type, lock.clientType, lock.clientId].join('_');
}
startAutoLockRefresh(lock: Lock, errorHandler: Function): string {
public async currentDate() {
return this.api_.remoteDate();
}
public startAutoLockRefresh(lock: Lock, errorHandler: Function): string {
const handle = this.autoLockRefreshHandle(lock);
if (this.refreshTimers_[handle]) {
throw new Error(`There is already a timer refreshing this lock: ${handle}`);
@@ -321,10 +351,11 @@ export default class LockHandler {
this.refreshTimers_[handle].inProgress = true;
let error = null;
const hasActiveLock = await this.hasActiveLock(lock.type, lock.clientType, lock.clientId);
if (!this.refreshTimers_[handle]) return defer(); // Timeout has been cleared
if (!hasActiveLock) {
const locks = await this.locks(lock.type);
if (!hasActiveLock(locks, await this.currentDate(), this.lockTtl, lock.type, lock.clientType, lock.clientId)) {
// If the previous lock has expired, we shouldn't try to acquire a new one. This is because other clients might have performed
// in the meantime operations that invalidates the current operation. For example, another client might have upgraded the
// sync target in the meantime, so any active operation should be cancelled here. Or if the current client was upgraded
@@ -384,6 +415,11 @@ export default class LockHandler {
}
public async releaseLock(lockType: LockType, clientType: string, clientId: string) {
if (this.useBuiltInLocks) {
await this.api_.releaseLock(lockType, clientType, clientId);
return;
}
await this.api_.delete(this.lockFilePath({
type: lockType,
clientType: clientType,

View File

@@ -1,4 +1,4 @@
import LockHandler, { LockType, LockHandlerOptions, Lock } from '../../services/synchronizer/LockHandler';
import LockHandler, { LockType, LockHandlerOptions, Lock, activeLock } from '../../services/synchronizer/LockHandler';
import { isNetworkSyncTarget, fileApi, setupDatabaseAndSynchronizer, synchronizer, switchClient, msleep, expectThrow, expectNotThrow } from '../../testing/test-utils';
// For tests with memory of file system we can use low intervals to make the tests faster.
@@ -46,6 +46,8 @@ describe('synchronizer_LockHandler', function() {
}));
it('should not use files that are not locks', (async () => {
if (lockHandler().useBuiltInLocks) return; // Doesn't make sense with built-in locks
await fileApi().put('locks/desktop.ini', 'a');
await fileApi().put('locks/exclusive.json', 'a');
await fileApi().put('locks/garbage.json', 'a');
@@ -75,10 +77,10 @@ describe('synchronizer_LockHandler', function() {
it('should auto-refresh a lock', (async () => {
const handler = newLockHandler({ autoRefreshInterval: 100 * timeoutMultipler });
const lock = await handler.acquireLock(LockType.Sync, 'desktop', '111');
const lockBefore = await handler.activeLock(LockType.Sync, 'desktop', '111');
const lockBefore = activeLock(await handler.locks(), new Date(), handler.lockTtl, LockType.Sync, 'desktop', '111');
handler.startAutoLockRefresh(lock, () => {});
await msleep(500 * timeoutMultipler);
const lockAfter = await handler.activeLock(LockType.Sync, 'desktop', '111');
const lockAfter = activeLock(await handler.locks(), new Date(), handler.lockTtl, LockType.Sync, 'desktop', '111');
expect(lockAfter.updatedTime).toBeGreaterThan(lockBefore.updatedTime);
handler.stopAutoLockRefresh(lock);
}));
@@ -95,11 +97,14 @@ describe('synchronizer_LockHandler', function() {
autoLockError = error;
});
await msleep(250 * timeoutMultipler);
try {
await msleep(250 * timeoutMultipler);
expect(autoLockError.code).toBe('lockExpired');
handler.stopAutoLockRefresh(lock);
expect(autoLockError).toBeTruthy();
expect(autoLockError.code).toBe('lockExpired');
} finally {
handler.stopAutoLockRefresh(lock);
}
}));
it('should not allow sync locks if there is an exclusive lock', (async () => {
@@ -112,6 +117,7 @@ describe('synchronizer_LockHandler', function() {
it('should not allow exclusive lock if there are sync locks', (async () => {
const lockHandler = newLockHandler({ lockTtl: 1000 * 60 * 60 });
if (lockHandler.useBuiltInLocks) return; // Tested server side
await lockHandler.acquireLock(LockType.Sync, 'mobile', '111');
await lockHandler.acquireLock(LockType.Sync, 'mobile', '222');
@@ -123,6 +129,7 @@ describe('synchronizer_LockHandler', function() {
it('should allow exclusive lock if the sync locks have expired', (async () => {
const lockHandler = newLockHandler({ lockTtl: 500 * timeoutMultipler });
if (lockHandler.useBuiltInLocks) return; // Tested server side
await lockHandler.acquireLock(LockType.Sync, 'mobile', '111');
await lockHandler.acquireLock(LockType.Sync, 'mobile', '222');
@@ -138,74 +145,44 @@ describe('synchronizer_LockHandler', function() {
const lockHandler = newLockHandler();
{
const lock1: Lock = { type: LockType.Exclusive, clientId: '1', clientType: 'd' };
const lock2: Lock = { type: LockType.Exclusive, clientId: '2', clientType: 'd' };
await lockHandler.saveLock_(lock1);
await msleep(100);
await lockHandler.saveLock_(lock2);
const locks: Lock[] = [
{
type: LockType.Exclusive,
clientId: '1',
clientType: 'd',
updatedTime: Date.now(),
},
];
const activeLock = await lockHandler.activeLock(LockType.Exclusive);
expect(activeLock.clientId).toBe('1');
await msleep(100);
locks.push({
type: LockType.Exclusive,
clientId: '2',
clientType: 'd',
updatedTime: Date.now(),
});
const lock = activeLock(locks, new Date(), lockHandler.lockTtl, LockType.Exclusive);
expect(lock.clientId).toBe('1');
}
}));
it('should ignore locks by same client when trying to acquire exclusive lock', (async () => {
const lockHandler = newLockHandler();
await lockHandler.acquireLock(LockType.Sync, 'desktop', '111');
await expectThrow(async () => {
await lockHandler.acquireLock(LockType.Exclusive, 'desktop', '111', { clearExistingSyncLocksFromTheSameClient: false });
}, 'hasSyncLock');
await expectNotThrow(async () => {
await lockHandler.acquireLock(LockType.Exclusive, 'desktop', '111', { clearExistingSyncLocksFromTheSameClient: true });
});
const activeLock = await lockHandler.activeLock(LockType.Exclusive);
expect(activeLock.clientId).toBe('111');
}));
// it('should not have race conditions', (async () => {
// it('should ignore locks by same client when trying to acquire exclusive lock', (async () => {
// const lockHandler = newLockHandler();
// const clients = [];
// for (let i = 0; i < 20; i++) {
// clients.push({
// id: 'client' + i,
// type: 'desktop',
// });
// }
// await lockHandler.acquireLock(LockType.Sync, 'desktop', '111');
// for (let loopIndex = 0; loopIndex < 1000; loopIndex++) {
// const promises:Promise<void | Lock>[] = [];
// for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
// const client = clients[clientIndex];
// await expectThrow(async () => {
// await lockHandler.acquireLock(LockType.Exclusive, 'desktop', '111', { clearExistingSyncLocksFromTheSameClient: false });
// }, 'hasSyncLock');
// promises.push(
// lockHandler.acquireLock(LockType.Exclusive, client.type, client.id).catch(() => {})
// );
// await expectNotThrow(async () => {
// await lockHandler.acquireLock(LockType.Exclusive, 'desktop', '111', { clearExistingSyncLocksFromTheSameClient: true });
// });
// // if (gotLock) {
// // await msleep(100);
// // const locks = await lockHandler.locks(LockType.Exclusive);
// // console.info('=======================================');
// // console.info(locks);
// // lockHandler.releaseLock(LockType.Exclusive, client.type, client.id);
// // }
// // await msleep(500);
// }
// const result = await Promise.all(promises);
// const locks = result.filter((lock:any) => !!lock);
// expect(locks.length).toBe(1);
// const lock:Lock = locks[0] as Lock;
// const allLocks = await lockHandler.locks();
// console.info('================================', allLocks);
// lockHandler.releaseLock(LockType.Exclusive, lock.clientType, lock.clientId);
// }
// const lock = activeLock(await lockHandler.locks(), new Date(), lockHandler.lockTtl, LockType.Exclusive);
// expect(lock.clientId).toBe('111');
// }));
});

View File

@@ -12,7 +12,7 @@ export async function allNotesFolders() {
async function remoteItemsByTypes(types: number[]) {
const list = await fileApi().list('', { includeDirs: false, syncItemsOnly: true });
if (list.has_more) throw new Error('Not implemented!!!');
if (list.hasMore) throw new Error('Not implemented!!!');
const files = list.items;
const output = [];

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/server",
"version": "2.5.10",
"version": "2.5.9",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/server",
"version": "2.5.10",
"version": "2.5.9",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@koa/cors": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.5.10",
"version": "2.5.9",
"private": true,
"scripts": {
"start-dev": "npm run build && nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",
@@ -11,7 +11,7 @@
"devDropTables": "node dist/app.js --env dev --drop-tables",
"devDropDb": "node dist/app.js --env dev --drop-db",
"start": "node dist/app.js",
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --migrate-latest --env buildTypes && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
"generateTypes": "rm -f db-buildTypes.sqlite && npm run start -- --env buildTypes migrate latest && node dist/tools/generateTypes.js && mv db-buildTypes.sqlite schema.sqlite",
"tsc": "tsc --project tsconfig.json",
"test": "jest --verbose=false",
"test-ci": "npm run test",

Binary file not shown.

View File

@@ -4,7 +4,7 @@ require('source-map-support').install();
import * as Koa from 'koa';
import * as fs from 'fs-extra';
import Logger, { LoggerWrapper, TargetType } from '@joplin/lib/Logger';
import config, { initConfig, runningInDocker } from './config';
import config, { initConfig, runningInDocker, EnvVariables } from './config';
import { migrateLatest, waitForConnection, sqliteDefaultDir } from './db';
import { AppContext, Env, KoaNext } from './utils/types';
import FsDriverNode from '@joplin/lib/fs-driver-node';
@@ -19,8 +19,6 @@ import apiVersionHandler from './middleware/apiVersionHandler';
import clickJackingHandler from './middleware/clickJackingHandler';
import newModelFactory from './models/factory';
import setupCommands from './utils/setupCommands';
import { RouteResponseFormat, routeResponseFormat } from './utils/routeUtils';
import { parseEnv } from './env';
interface Argv {
env?: Env;
@@ -34,7 +32,7 @@ const nodeEnvFile = require('node-env-file');
const { shimInit } = require('@joplin/lib/shim-init-node.js');
shimInit({ nodeSqlite });
const defaultEnvVariables: Record<Env, any> = {
const defaultEnvVariables: Record<Env, EnvVariables> = {
dev: {
// To test with the Postgres database, uncomment DB_CLIENT below and
// comment out SQLITE_DATABASE. Then start the Postgres server using
@@ -96,7 +94,10 @@ async function main() {
if (!defaultEnvVariables[env]) throw new Error(`Invalid env: ${env}`);
const envVariables = parseEnv(process.env, defaultEnvVariables[env]);
const envVariables: EnvVariables = {
...defaultEnvVariables[env],
...process.env,
};
const app = new Koa();
@@ -138,28 +139,17 @@ async function main() {
} catch (error) {
ctx.status = error.httpCode || 500;
appLogger().error(`Middleware error on ${ctx.path}:`, error);
const responseFormat = routeResponseFormat(ctx);
if (responseFormat === RouteResponseFormat.Html) {
// Since this is a low level error, rendering a view might fail too,
// so catch this and default to rendering JSON.
try {
ctx.response.set('Content-Type', 'text/html');
ctx.body = await ctx.joplin.services.mustache.renderView({
name: 'error',
title: 'Error',
path: 'index/error',
content: { error },
});
} catch (anotherError) {
ctx.response.set('Content-Type', 'application/json');
ctx.body = JSON.stringify({ error: error.message });
}
} else {
ctx.response.set('Content-Type', 'application/json');
ctx.body = JSON.stringify({ error: error.message });
// Since this is a low level error, rendering a view might fail too,
// so catch this and default to rendering JSON.
try {
ctx.body = await ctx.joplin.services.mustache.renderView({
name: 'error',
title: 'Error',
path: 'index/error',
content: { error },
});
} catch (anotherError) {
ctx.body = { error: anotherError.message };
}
}
});
@@ -252,7 +242,6 @@ async function main() {
appLogger().info('User content base URL:', config().userContentBaseUrl);
appLogger().info('Log dir:', config().logDir);
appLogger().info('DB Config:', markPasswords(config().database));
appLogger().info('Mailer Config:', markPasswords(config().mailer));
appLogger().info('Trying to connect to database...');
const connectionCheck = await waitForConnection(config().database);

View File

@@ -2,7 +2,6 @@ import { rtrimSlashes } from '@joplin/lib/path-utils';
import { Config, DatabaseConfig, DatabaseConfigClient, Env, MailerConfig, RouteType, StripeConfig } from './utils/types';
import * as pathUtils from 'path';
import { loadStripeConfig, StripePublicConfig } from '@joplin/lib/utils/joplinCloud';
import { EnvVariables } from './env';
interface PackageJson {
version: string;
@@ -10,12 +9,95 @@ interface PackageJson {
const packageJson: PackageJson = require(`${__dirname}/packageInfo.js`);
export interface EnvVariables {
// ==================================================
// General config
// ==================================================
APP_NAME?: string;
APP_PORT?: string;
SIGNUP_ENABLED?: string;
TERMS_ENABLED?: string;
ACCOUNT_TYPES_ENABLED?: string;
ERROR_STACK_TRACES?: string;
COOKIES_SECURE?: string;
RUNNING_IN_DOCKER?: string;
BUILTIN_LOCKS_ENABLED?: string;
// ==================================================
// URL config
// ==================================================
APP_BASE_URL?: string;
USER_CONTENT_BASE_URL?: string;
API_BASE_URL?: string;
JOPLINAPP_BASE_URL?: string;
// ==================================================
// Database config
// ==================================================
DB_CLIENT?: string;
DB_SLOW_QUERY_LOG_ENABLED?: string;
DB_SLOW_QUERY_LOG_MIN_DURATION?: string; // ms
DB_AUTO_MIGRATION?: string;
POSTGRES_PASSWORD?: string;
POSTGRES_DATABASE?: string;
POSTGRES_USER?: string;
POSTGRES_HOST?: string;
POSTGRES_PORT?: string;
// This must be the full path to the database file
SQLITE_DATABASE?: string;
// ==================================================
// Mailer config
// ==================================================
MAILER_ENABLED?: string;
MAILER_HOST?: string;
MAILER_PORT?: string;
MAILER_SECURE?: string;
MAILER_AUTH_USER?: string;
MAILER_AUTH_PASSWORD?: string;
MAILER_NOREPLY_NAME?: string;
MAILER_NOREPLY_EMAIL?: string;
SUPPORT_EMAIL?: string;
SUPPORT_NAME?: string;
BUSINESS_EMAIL?: string;
// ==================================================
// Stripe config
// ==================================================
STRIPE_SECRET_KEY?: string;
STRIPE_WEBHOOK_SECRET?: string;
}
let runningInDocker_: boolean = false;
export function runningInDocker(): boolean {
return runningInDocker_;
}
function envReadString(s: string, defaultValue: string = ''): string {
return s === undefined || s === null ? defaultValue : s;
}
function envReadBool(s: string, defaultValue = false): boolean {
if (s === undefined || s === null) return defaultValue;
return s === '1';
}
function envReadInt(s: string, defaultValue: number = null): number {
if (!s) return defaultValue === null ? 0 : defaultValue;
const output = Number(s);
if (isNaN(output)) throw new Error(`Invalid number: ${s}`);
return output;
}
function databaseHostFromEnv(runningInDocker: boolean, env: EnvVariables): string {
if (env.POSTGRES_HOST) {
// When running within Docker, the app localhost is different from the
@@ -35,19 +117,19 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
const baseConfig: DatabaseConfig = {
client: DatabaseConfigClient.Null,
name: '',
slowQueryLogEnabled: env.DB_SLOW_QUERY_LOG_ENABLED,
slowQueryLogMinDuration: env.DB_SLOW_QUERY_LOG_MIN_DURATION,
autoMigration: env.DB_AUTO_MIGRATION,
slowQueryLogEnabled: envReadBool(env.DB_SLOW_QUERY_LOG_ENABLED),
slowQueryLogMinDuration: envReadInt(env.DB_SLOW_QUERY_LOG_MIN_DURATION, 10000),
autoMigration: envReadBool(env.DB_AUTO_MIGRATION, true),
};
if (env.DB_CLIENT === 'pg') {
return {
...baseConfig,
client: DatabaseConfigClient.PostgreSQL,
name: env.POSTGRES_DATABASE,
user: env.POSTGRES_USER,
password: env.POSTGRES_PASSWORD,
port: env.POSTGRES_PORT,
name: env.POSTGRES_DATABASE || 'joplin',
user: env.POSTGRES_USER || 'joplin',
password: env.POSTGRES_PASSWORD || 'joplin',
port: env.POSTGRES_PORT ? Number(env.POSTGRES_PORT) : 5432,
host: databaseHostFromEnv(runningInDocker, env) || 'localhost',
};
}
@@ -62,14 +144,14 @@ function databaseConfigFromEnv(runningInDocker: boolean, env: EnvVariables): Dat
function mailerConfigFromEnv(env: EnvVariables): MailerConfig {
return {
enabled: env.MAILER_ENABLED,
host: env.MAILER_HOST,
port: env.MAILER_PORT,
secure: env.MAILER_SECURE,
authUser: env.MAILER_AUTH_USER,
authPassword: env.MAILER_AUTH_PASSWORD,
noReplyName: env.MAILER_NOREPLY_NAME,
noReplyEmail: env.MAILER_NOREPLY_EMAIL,
enabled: env.MAILER_ENABLED !== '0',
host: env.MAILER_HOST || '',
port: Number(env.MAILER_PORT || 587),
secure: !!Number(env.MAILER_SECURE) || true,
authUser: env.MAILER_AUTH_USER || '',
authPassword: env.MAILER_AUTH_PASSWORD || '',
noReplyName: env.MAILER_NOREPLY_NAME || '',
noReplyEmail: env.MAILER_NOREPLY_EMAIL || '',
};
}
@@ -77,12 +159,12 @@ function stripeConfigFromEnv(publicConfig: StripePublicConfig, env: EnvVariables
return {
...publicConfig,
enabled: !!env.STRIPE_SECRET_KEY,
secretKey: env.STRIPE_SECRET_KEY,
webhookSecret: env.STRIPE_WEBHOOK_SECRET,
secretKey: env.STRIPE_SECRET_KEY || '',
webhookSecret: env.STRIPE_WEBHOOK_SECRET || '',
};
}
function baseUrlFromEnv(env: EnvVariables, appPort: number): string {
function baseUrlFromEnv(env: any, appPort: number): string {
if (env.APP_BASE_URL) {
return rtrimSlashes(env.APP_BASE_URL);
} else {
@@ -97,12 +179,12 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
const rootDir = pathUtils.dirname(__dirname);
const stripePublicConfig = loadStripeConfig(envType === Env.BuildTypes ? Env.Dev : envType, `${rootDir}/stripeConfig.json`);
const appName = env.APP_NAME;
const appName = env.APP_NAME || 'Joplin Server';
const viewDir = `${rootDir}/src/views`;
const appPort = env.APP_PORT;
const appPort = env.APP_PORT ? Number(env.APP_PORT) : 22300;
const baseUrl = baseUrlFromEnv(env, appPort);
const apiBaseUrl = env.API_BASE_URL ? env.API_BASE_URL : baseUrl;
const supportEmail = env.SUPPORT_EMAIL;
const supportEmail = env.SUPPORT_EMAIL || 'SUPPORT_EMAIL'; // Defaults to "SUPPORT_EMAIL" so that server admin knows they have to set it.
config_ = {
appVersion: packageJson.version,
@@ -119,17 +201,18 @@ export async function initConfig(envType: Env, env: EnvVariables, overrides: any
stripe: stripeConfigFromEnv(stripePublicConfig, env),
port: appPort,
baseUrl,
showErrorStackTraces: env.ERROR_STACK_TRACES,
showErrorStackTraces: (env.ERROR_STACK_TRACES === undefined && envType === Env.Dev) || env.ERROR_STACK_TRACES === '1',
apiBaseUrl,
userContentBaseUrl: env.USER_CONTENT_BASE_URL ? env.USER_CONTENT_BASE_URL : baseUrl,
joplinAppBaseUrl: env.JOPLINAPP_BASE_URL,
signupEnabled: env.SIGNUP_ENABLED,
termsEnabled: env.TERMS_ENABLED,
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED,
joplinAppBaseUrl: envReadString(env.JOPLINAPP_BASE_URL, 'https://joplinapp.org'),
signupEnabled: env.SIGNUP_ENABLED === '1',
termsEnabled: env.TERMS_ENABLED === '1',
accountTypesEnabled: env.ACCOUNT_TYPES_ENABLED === '1',
supportEmail,
supportName: env.SUPPORT_NAME || appName,
businessEmail: env.BUSINESS_EMAIL || supportEmail,
cookieSecure: env.COOKIES_SECURE,
cookieSecure: env.COOKIES_SECURE === '1',
buildInLocksEnabled: envReadBool(env.BUILTIN_LOCKS_ENABLED, false),
...overrides,
};
}

View File

@@ -116,6 +116,10 @@ export const clientType = (db: DbConnection): DatabaseConfigClient => {
return db.client.config.client;
};
export const returningSupported = (db: DbConnection) => {
return clientType(db) === DatabaseConfigClient.PostgreSQL;
};
export const isPostgres = (db: DbConnection) => {
return clientType(db) === DatabaseConfigClient.PostgreSQL;
};

View File

@@ -1,138 +0,0 @@
export interface EnvVariables {
// ==================================================
// General config
// ==================================================
APP_NAME: string;
APP_PORT: number;
SIGNUP_ENABLED: boolean;
TERMS_ENABLED: boolean;
ACCOUNT_TYPES_ENABLED: boolean;
ERROR_STACK_TRACES: boolean;
COOKIES_SECURE: boolean;
RUNNING_IN_DOCKER: boolean;
// ==================================================
// URL config
// ==================================================
APP_BASE_URL: string;
USER_CONTENT_BASE_URL: string;
API_BASE_URL: string;
JOPLINAPP_BASE_URL: string;
// ==================================================
// Database config
// ==================================================
DB_CLIENT: string;
DB_SLOW_QUERY_LOG_ENABLED: boolean;
DB_SLOW_QUERY_LOG_MIN_DURATION: number;
DB_AUTO_MIGRATION: boolean;
POSTGRES_PASSWORD: string;
POSTGRES_DATABASE: string;
POSTGRES_USER: string;
POSTGRES_HOST: string;
POSTGRES_PORT: number;
// This must be the full path to the database file
SQLITE_DATABASE: string;
// ==================================================
// Mailer config
// ==================================================
MAILER_ENABLED: boolean;
MAILER_HOST: string;
MAILER_PORT: number;
MAILER_SECURE: boolean;
MAILER_AUTH_USER: string;
MAILER_AUTH_PASSWORD: string;
MAILER_NOREPLY_NAME: string;
MAILER_NOREPLY_EMAIL: string;
SUPPORT_EMAIL: string;
SUPPORT_NAME: string;
BUSINESS_EMAIL: string;
// ==================================================
// Stripe config
// ==================================================
STRIPE_SECRET_KEY: string;
STRIPE_WEBHOOK_SECRET: string;
}
const defaultEnvValues: EnvVariables = {
APP_NAME: 'Joplin Server',
APP_PORT: 22300,
SIGNUP_ENABLED: false,
TERMS_ENABLED: false,
ACCOUNT_TYPES_ENABLED: false,
ERROR_STACK_TRACES: false,
COOKIES_SECURE: false,
RUNNING_IN_DOCKER: false,
APP_BASE_URL: '',
USER_CONTENT_BASE_URL: '',
API_BASE_URL: '',
JOPLINAPP_BASE_URL: 'https://joplinapp.org',
DB_CLIENT: 'sqlite3',
DB_SLOW_QUERY_LOG_ENABLED: false,
DB_SLOW_QUERY_LOG_MIN_DURATION: 1000,
DB_AUTO_MIGRATION: true,
POSTGRES_PASSWORD: 'joplin',
POSTGRES_DATABASE: 'joplin',
POSTGRES_USER: 'joplin',
POSTGRES_HOST: '',
POSTGRES_PORT: 5432,
SQLITE_DATABASE: '',
MAILER_ENABLED: false,
MAILER_HOST: '',
MAILER_PORT: 587,
MAILER_SECURE: true,
MAILER_AUTH_USER: '',
MAILER_AUTH_PASSWORD: '',
MAILER_NOREPLY_NAME: '',
MAILER_NOREPLY_EMAIL: '',
SUPPORT_EMAIL: 'SUPPORT_EMAIL', // Defaults to "SUPPORT_EMAIL" so that server admin knows they have to set it.
SUPPORT_NAME: '',
BUSINESS_EMAIL: '',
STRIPE_SECRET_KEY: '',
STRIPE_WEBHOOK_SECRET: '',
};
export function parseEnv(rawEnv: any, defaultOverrides: any = null): EnvVariables {
const output: EnvVariables = {
...defaultEnvValues,
...defaultOverrides,
};
for (const [key, value] of Object.entries(defaultEnvValues)) {
const rawEnvValue = rawEnv[key];
if (rawEnvValue === undefined) continue;
if (typeof value === 'number') {
const v = Number(rawEnvValue);
if (isNaN(v)) throw new Error(`Invalid number value for env variable ${key} = ${rawEnvValue}`);
(output as any)[key] = v;
} else if (typeof value === 'boolean') {
if (rawEnvValue !== '0' && rawEnvValue !== '1') throw new Error(`Invalid boolean for for env variable ${key}: ${rawEnvValue}`);
(output as any)[key] = rawEnvValue === '1';
} else if (typeof value === 'string') {
(output as any)[key] = `${rawEnvValue}`;
} else {
throw new Error(`Invalid env default value type: ${typeof value}`);
}
}
return output;
}

View File

@@ -0,0 +1,23 @@
import { Knex } from 'knex';
import { DbConnection } from '../db';
export async function up(db: DbConnection): Promise<any> {
await db.schema.createTable('locks', (table: Knex.CreateTableBuilder) => {
table.uuid('id').unique().primary().notNullable();
table.string('user_id', 32).notNullable();
table.integer('type', 2).notNullable();
table.string('client_type', 32).notNullable();
table.string('client_id', 32).notNullable();
table.bigInteger('updated_time').notNullable();
table.bigInteger('created_time').notNullable();
});
await db.schema.alterTable('locks', (table: Knex.CreateTableBuilder) => {
table.index('user_id');
table.index('created_time');
});
}
export async function down(db: DbConnection): Promise<any> {
await db.schema.dropTable('locks');
}

View File

@@ -95,6 +95,10 @@ export default abstract class BaseModel<T> {
return this.db_;
}
protected get dbRead(): DbConnection {
return this.db;
}
protected get defaultFields(): string[] {
if (!this.defaultFields_.length) {
this.defaultFields_ = Object.keys(databaseSchema[this.tableName]);

View File

@@ -1,4 +1,6 @@
import { returningSupported } from '../db';
import { KeyValue } from '../services/database/types';
import { msleep } from '../utils/time';
import BaseModel from './BaseModel';
export enum ValueType {
@@ -6,7 +8,9 @@ export enum ValueType {
String = 2,
}
type Value = number | string;
export type Value = number | string;
export type ReadThenWriteHandler = (value: Value)=> Promise<Value>;
export default class KeyValueModel extends BaseModel<KeyValue> {
@@ -57,6 +61,45 @@ export default class KeyValueModel extends BaseModel<KeyValue> {
return this.unserializeValue(row.type, row.value) as any;
}
public async readThenWrite(key: string, handler: ReadThenWriteHandler) {
if (!returningSupported(this.db)) {
// While inside a transaction SQlite should lock the whole database
// file, which should allow atomic read then write.
await this.withTransaction(async () => {
const value: any = await this.value(key);
const newValue = await handler(value);
await this.setValue(key, newValue);
}, 'KeyValueModel::readThenWrite');
return;
}
let loopCount = 0;
while (true) {
const row: KeyValue = await this.db(this.tableName).where('key', '=', key).first();
const newValue = await handler(row ? row.value : null);
let previousValue: Value = null;
if (row) {
previousValue = row.value;
} else {
await this.setValue(key, newValue);
previousValue = newValue;
}
const updatedRows = await this
.db(this.tableName)
.update({ value: newValue }, ['id'])
.where('key', '=', key)
.where('value', '=', previousValue);
if (updatedRows.length) return;
loopCount++;
if (loopCount >= 10) throw new Error(`Could not update key: ${key}`);
await msleep(10000 * Math.random());
}
}
public async deleteValue(key: string): Promise<void> {
await this.db(this.tableName).where('key', '=', key).delete();
}
@@ -65,4 +108,8 @@ export default class KeyValueModel extends BaseModel<KeyValue> {
throw new Error('Call ::deleteValue()');
}
public async deleteAll(): Promise<void> {
await this.db(this.tableName).delete();
}
}

View File

@@ -0,0 +1,150 @@
import BaseModel, { UuidType } from './BaseModel';
import { Uuid } from '../services/database/types';
import { Lock, LockType, lockDefaultTtl, activeLock } from '@joplin/lib/services/synchronizer/LockHandler';
import { Value } from './KeyValueModel';
import { ErrorConflict } from '../utils/errors';
import uuidgen from '../utils/uuidgen';
export default class LockModel extends BaseModel<Lock> {
private lockTtl_: number = lockDefaultTtl;
protected get tableName(): string {
return 'locks';
}
protected uuidType(): UuidType {
return UuidType.Native;
}
// TODO: validate lock when acquiring and releasing
// TODO: test "should allow exclusive lock if the sync locks have expired"
// TODO: test "should not allow exclusive lock if there are sync locks"
private get lockTtl() {
return this.lockTtl_;
}
public async allLocks(userId: Uuid): Promise<Lock[]> {
const userKey = `locks::${userId}`;
const v = await this.models().keyValue().value<string>(userKey);
return v ? JSON.parse(v) : [];
}
private async acquireSyncLock(userId: Uuid, clientType: string, clientId: string): Promise<Lock> {
const userKey = `locks::${userId}`;
let output: Lock = null;
await this.models().keyValue().readThenWrite(userKey, async (value: Value) => {
let locks: Lock[] = value ? JSON.parse(value as string) : [];
const exclusiveLock = activeLock(locks, new Date(), this.lockTtl, LockType.Exclusive);
if (exclusiveLock) {
throw new ErrorConflict(`Cannot acquire lock because there is already an exclusive lock for client: ${exclusiveLock.clientType} #${exclusiveLock.clientId}`, 'hasExclusiveLock');
}
const syncLock = activeLock(locks, new Date(), this.lockTtl, LockType.Sync, clientType, clientId);
if (syncLock) {
output = {
...syncLock,
updatedTime: Date.now(),
};
locks = locks.map(l => l.id === syncLock.id ? output : l);
} else {
output = {
id: uuidgen(),
type: LockType.Sync,
clientId,
clientType,
updatedTime: Date.now(),
};
locks.push(output);
}
return JSON.stringify(locks);
});
return output;
}
private async acquireExclusiveLock(userId: Uuid, clientType: string, clientId: string): Promise<Lock> {
const userKey = `locks::${userId}`;
let output: Lock = null;
await this.models().keyValue().readThenWrite(userKey, async (value: Value) => {
let locks: Lock[] = value ? JSON.parse(value as string) : [];
const exclusiveLock = activeLock(locks, new Date(), this.lockTtl, LockType.Exclusive);
if (exclusiveLock) {
if (exclusiveLock.clientId === clientId) {
locks = locks.filter(l => l.id !== exclusiveLock.id);
output = {
...exclusiveLock,
updatedTime: Date.now(),
};
locks.push(output);
return JSON.stringify(locks);
} else {
throw new ErrorConflict(`Cannot acquire lock because there is already an exclusive lock for client: ${exclusiveLock.clientType} #${exclusiveLock.clientId}`, 'hasExclusiveLock');
}
}
const syncLock = activeLock(locks, new Date(), this.lockTtl, LockType.Sync);
if (syncLock) {
if (syncLock.clientId === clientId) {
locks = locks.filter(l => l.id !== syncLock.id);
} else {
throw new ErrorConflict(`Cannot acquire exclusive lock because there is an active sync lock for client: ${syncLock.clientType} #${syncLock.clientId}`, 'hasSyncLock');
}
}
output = {
id: uuidgen(),
type: LockType.Exclusive,
clientId,
clientType,
updatedTime: Date.now(),
};
locks.push(output);
return JSON.stringify(locks);
});
return output;
}
public async acquireLock(userId: Uuid, type: LockType, clientType: string, clientId: string): Promise<Lock> {
if (type === LockType.Sync) {
return this.acquireSyncLock(userId, clientType, clientId);
} else {
return this.acquireExclusiveLock(userId, clientType, clientId);
}
}
public async releaseLock(userId: Uuid, lockType: LockType, clientType: string, clientId: string) {
const userKey = `locks::${userId}`;
await this.models().keyValue().readThenWrite(userKey, async (value: Value) => {
const locks: Lock[] = value ? JSON.parse(value as string) : [];
for (let i = locks.length - 1; i >= 0; i--) {
const lock = locks[i];
if (lock.type === lockType && lock.clientType === clientType && lock.clientId === clientId) {
locks.splice(i, 1);
}
}
return JSON.stringify(locks);
});
}
}

View File

@@ -72,6 +72,7 @@ import SubscriptionModel from './SubscriptionModel';
import UserFlagModel from './UserFlagModel';
import EventModel from './EventModel';
import { Config } from '../utils/types';
import LockModel from './LockModel';
export class Models {
@@ -147,6 +148,10 @@ export class Models {
return new EventModel(this.db_, newModelFactory, this.config_);
}
public lock() {
return new LockModel(this.db_, newModelFactory, this.config_);
}
}
export default function newModelFactory(db: DbConnection, config: Config): Models {

View File

@@ -2,9 +2,10 @@ import config from '../../config';
import { clearDatabase, createTestUsers, CreateTestUsersOptions } from '../../tools/debugTools';
import { bodyFields } from '../../utils/requestUtils';
import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { Env, RouteType } from '../../utils/types';
import { SubPath } from '../../utils/routeUtils';
import { AppContext } from '../../utils/types';
import { ErrorForbidden } from '../../utils/errors';
const router = new Router(RouteType.Api);
@@ -17,7 +18,10 @@ interface Query {
}
router.post('api/debug', async (_path: SubPath, ctx: AppContext) => {
if (config().env !== Env.Dev) throw new ErrorForbidden();
const query: Query = (await bodyFields(ctx.req)) as Query;
const models = ctx.joplin.models;
console.info(`Action: ${query.action}`);
@@ -33,6 +37,10 @@ router.post('api/debug', async (_path: SubPath, ctx: AppContext) => {
if (query.action === 'clearDatabase') {
await clearDatabase(ctx.joplin.db);
}
if (query.action === 'clearKeyValues') {
await models.keyValue().deleteAll();
}
});
export default router;

View File

@@ -11,6 +11,7 @@ import { requestDeltaPagination, requestPagination } from '../../models/utils/pa
import { AclAction } from '../../models/BaseModel';
import { safeRemove } from '../../utils/fileUtils';
import { formatBytes, MB } from '../../utils/bytes';
import lockHandler from './utils/items/lockHandler';
const router = new Router(RouteType.Api);
@@ -42,6 +43,9 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b
try {
const buffer = filePath ? await fs.readFile(filePath) : Buffer.alloc(0);
const lockResult = await lockHandler(path, ctx, buffer);
if (lockResult.handled) return lockResult.response;
// This end point can optionally set the associated jop_share_id field. It
// is only useful when uploading resource blob (under .resource folder)
// since they can't have metadata. Note, Folder and Resource items all
@@ -104,8 +108,13 @@ router.del('api/items/:id', async (path: SubPath, ctx: AppContext) => {
if (ctx.joplin.env !== 'dev') throw new ErrorMethodNotAllowed('Deleting the root is not allowed');
await ctx.joplin.models.item().deleteAll(ctx.joplin.owner.id);
} else {
// const item = await itemFromPath(ctx.joplin.owner.id, ctx.joplin.models.item(), path);
// await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, item);
const lockResult = await lockHandler(path, ctx);
if (lockResult.handled) return lockResult.response;
const item = await itemFromPath(ctx.joplin.owner.id, ctx.joplin.models.item(), path);
await ctx.joplin.models.item().checkIfAllowed(ctx.joplin.owner, AclAction.Delete, item);
await ctx.joplin.models.item().deleteForUser(ctx.joplin.owner.id, item);
}
} catch (error) {
@@ -137,6 +146,9 @@ router.get('api/items/:id/delta', async (_path: SubPath, ctx: AppContext) => {
});
router.get('api/items/:id/children', async (path: SubPath, ctx: AppContext) => {
const lockResult = await lockHandler(path, ctx);
if (lockResult.handled) return lockResult.response;
const itemModel = ctx.joplin.models.item();
const parentName = itemModel.pathToName(path.id);
const result = await itemModel.children(ctx.joplin.owner.id, parentName, requestPagination(ctx.query));

View File

@@ -0,0 +1,32 @@
import { lockNameToObject, LockType } from '@joplin/lib/services/synchronizer/LockHandler';
import { bodyFields } from '../../utils/requestUtils';
import Router from '../../utils/Router';
import { SubPath } from '../../utils/routeUtils';
import { AppContext, RouteType } from '../../utils/types';
const router = new Router(RouteType.Api);
interface PostFields {
type: LockType;
clientType: string;
clientId: string;
}
router.post('api/locks', async (_path: SubPath, ctx: AppContext) => {
const fields = await bodyFields<PostFields>(ctx.req);
return ctx.joplin.models.lock().acquireLock(ctx.joplin.owner.id, fields.type, fields.clientType, fields.clientId);
});
router.del('api/locks/:id', async (path: SubPath, ctx: AppContext) => {
const lock = lockNameToObject(path.id);
await ctx.joplin.models.lock().releaseLock(ctx.joplin.owner.id, lock.type, lock.clientType, lock.clientId);
});
router.get('api/locks', async (_path: SubPath, ctx: AppContext) => {
return {
items: await ctx.joplin.models.lock().allLocks(ctx.joplin.owner.id),
has_more: false,
};
});
export default router;

View File

@@ -0,0 +1,47 @@
import config from '../../../../config';
import { PaginatedItems } from '../../../../models/ItemModel';
import { Item } from '../../../../services/database/types';
import { getApi, putApi } from '../../../../utils/testing/apiUtils';
import { beforeAllDb, afterAllTests, beforeEachDb, createUserAndSession, models } from '../../../../utils/testing/testUtils';
describe('items/lockHandlers', function() {
beforeAll(async () => {
await beforeAllDb('items/lockHandlers');
});
afterAll(async () => {
await afterAllTests();
});
beforeEach(async () => {
await beforeEachDb();
config().buildInLocksEnabled = true;
});
test('should save locks to the key-value store', async function() {
const { session, user } = await createUserAndSession(1);
const lockName = 'locks/exclusive_cli_12cb74fa9de644958b2ccbc772cb4e29.json';
const now = Date.now();
const result: Item = await putApi(session.id, `items/root:/${lockName}:/content`, { testing: true });
expect(result.name).toBe(lockName);
expect(result.updated_time).toBeGreaterThanOrEqual(now);
expect(result.id).toBe(null);
const values = await models().keyValue().all();
expect(values.length).toBe(1);
expect(values[0].key).toBe(`locks::${user.id}`);
const value = JSON.parse(values[0].value);
expect(value[lockName].name).toBe(lockName);
expect(value[lockName].updated_time).toBeGreaterThanOrEqual(now);
const getResult: PaginatedItems = await getApi(session.id, 'items/root:/locks/*:children');
console.info(getResult);
expect(getResult.items[0].name).toBe(result.name);
expect(getResult.items[0].updated_time).toBe(result.updated_time);
});
});

View File

@@ -0,0 +1,98 @@
import config from '../../../../config';
import { PaginatedItems } from '../../../../models/ItemModel';
import { Value } from '../../../../models/KeyValueModel';
import { Item } from '../../../../services/database/types';
import { ErrorBadRequest } from '../../../../utils/errors';
import { SubPath } from '../../../../utils/routeUtils';
import { AppContext } from '../../../../utils/types';
interface LockHandlerResult {
handled: boolean;
response: any;
}
const lockHandler = async (path: SubPath, ctx: AppContext, requestBody: Buffer = null): Promise<LockHandlerResult | null> => {
if (!config().buildInLocksEnabled) return { handled: false, response: null };
if (!path.id || !path.id.startsWith('root:/locks/')) return { handled: false, response: null };
const ownerId = ctx.joplin.owner.id;
const models = ctx.joplin.models;
const userKey = `locks::${ownerId}`;
// PUT /api/items/root:/locks/exclusive_cli_12cb74fa9de644958b2ccbc772cb4e29.json:/content
if (ctx.method === 'PUT') {
const itemName = models.item().pathToName(path.id);
const now = Date.now();
await models.keyValue().readThenWrite(userKey, async (value: Value) => {
const output = value ? JSON.parse(value as string) : {};
output[itemName] = {
name: itemName,
updated_time: now,
jop_updated_time: now,
content: requestBody.toString(),
};
return JSON.stringify(output);
});
return {
handled: true,
response: {
[itemName]: {
item: {
name: itemName,
updated_time: now,
id: null,
},
error: null,
},
},
};
}
// DELETE /api/items/root:/locks/exclusive_cli_12cb74fa9de644958b2ccbc772cb4e29.json:
if (ctx.method === 'DELETE') {
const itemName = models.item().pathToName(path.id);
await models.keyValue().readThenWrite(userKey, async (value: Value) => {
const output = value ? JSON.parse(value as string) : {};
delete output[itemName];
return JSON.stringify(output);
});
return {
handled: true,
response: null,
};
}
// GET /api/items/root:/locks/*:/children
if (ctx.method === 'GET' && path.id === 'root:/locks/*:') {
const result = await models.keyValue().value<string>(userKey);
const obj: Record<string, Item> = result ? JSON.parse(result) : {};
const items: Item[] = [];
for (const name of Object.keys(obj)) {
items.push(obj[name]);
}
const page: PaginatedItems = {
has_more: false,
items,
};
return {
handled: true,
response: page,
};
}
throw new ErrorBadRequest(`Unhandled lock path: ${path.id}`);
};
export default lockHandler;

View File

@@ -10,6 +10,7 @@ import apiSessions from './api/sessions';
import apiShares from './api/shares';
import apiShareUsers from './api/share_users';
import apiUsers from './api/users';
import apiLocks from './api/locks';
import indexChanges from './index/changes';
import indexHelp from './index/help';
@@ -41,6 +42,7 @@ const routes: Routers = {
'api/share_users': apiShareUsers,
'api/shares': apiShares,
'api/users': apiUsers,
'api/locks': apiLocks,
'changes': indexChanges,
'home': indexHome,

View File

@@ -258,6 +258,16 @@ export interface Item extends WithDates, WithUuid {
owner_id?: Uuid;
}
export interface Lock {
id?: Uuid;
user_id?: Uuid;
type?: number;
client_type?: string;
client_id?: Uuid;
updated_time?: string;
created_time?: string;
}
export const databaseSchema: DatabaseTables = {
sessions: {
id: { type: 'string' },
@@ -430,5 +440,14 @@ export const databaseSchema: DatabaseTables = {
jop_updated_time: { type: 'string' },
owner_id: { type: 'string' },
},
locks: {
id: { type: 'string' },
user_id: { type: 'string' },
type: { type: 'number' },
client_type: { type: 'string' },
client_id: { type: 'string' },
updated_time: { type: 'string' },
created_time: { type: 'string' },
},
};
// AUTO-GENERATED-TYPES

View File

@@ -79,8 +79,8 @@ export class ErrorUnprocessableEntity extends ApiError {
export class ErrorConflict extends ApiError {
public static httpCode: number = 409;
public constructor(message: string = 'Conflict') {
super(message, ErrorConflict.httpCode);
public constructor(message: string = 'Conflict', code: string = undefined) {
super(message, ErrorConflict.httpCode, code);
Object.setPrototypeOf(this, ErrorConflict.prototype);
}
}

View File

@@ -86,7 +86,7 @@ export async function beforeAllDb(unitName: string, createDbOptions: CreateDbOpt
await initConfig(Env.Dev, {
SQLITE_DATABASE: createdDbPath_,
SUPPORT_EMAIL: 'testing@localhost',
} as any, {
}, {
tempDir: tempDir,
});

View File

@@ -115,6 +115,7 @@ export interface Config {
businessEmail: string;
isJoplinCloud: boolean;
cookieSecure: boolean;
buildInLocksEnabled: boolean;
}
export enum HttpMethod {

View File

@@ -1,10 +1,5 @@
# Joplin Server Changelog
## [server-v2.5.10](https://github.com/laurent22/joplin/releases/tag/server-v2.5.10) - 2021-11-02T14:45:54Z
- New: Add unique constraint on name and owner ID of items table (f7a18ba)
- Fixed: Fixed issue that could cause server to return empty items in some rare cases (99ea4b7)
## [server-v2.5.9](https://github.com/laurent22/joplin/releases/tag/server-v2.5.9) - 2021-10-28T19:43:41Z
- Improved: Remove session expiration for now (4a2af32)