chore: add custom folder prompt (#3961)

* chore: add custom folder prompt

* chore: zip collab db

* chore: fix test

* chore: add test

* chore: fmt

* chore: fmt

* chore: fmt
This commit is contained in:
Nathan.fooo 2023-11-20 20:54:47 +08:00 committed by GitHub
parent 6f83f41c2d
commit b9ecc7ceb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
67 changed files with 1423 additions and 551 deletions

View File

@ -4,21 +4,27 @@ import 'package:json_annotation/json_annotation.dart';
part 'backend_env.g.dart';
@JsonSerializable()
class AppFlowyEnv {
class AppFlowyConfiguration {
final String custom_app_path;
final String origin_app_path;
final String device_id;
final int cloud_type;
final SupabaseConfiguration supabase_config;
final AppFlowyCloudConfiguration appflowy_cloud_config;
AppFlowyEnv({
AppFlowyConfiguration({
required this.custom_app_path,
required this.origin_app_path,
required this.device_id,
required this.cloud_type,
required this.supabase_config,
required this.appflowy_cloud_config,
});
factory AppFlowyEnv.fromJson(Map<String, dynamic> json) =>
_$AppFlowyEnvFromJson(json);
factory AppFlowyConfiguration.fromJson(Map<String, dynamic> json) =>
_$AppFlowyConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$AppFlowyEnvToJson(this);
Map<String, dynamic> toJson() => _$AppFlowyConfigurationToJson(this);
}
@JsonSerializable()
@ -36,6 +42,13 @@ class SupabaseConfiguration {
_$SupabaseConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$SupabaseConfigurationToJson(this);
static SupabaseConfiguration defaultConfig() {
return SupabaseConfiguration(
url: '',
anon_key: '',
);
}
}
@JsonSerializable()
@ -54,4 +67,12 @@ class AppFlowyCloudConfiguration {
_$AppFlowyCloudConfigurationFromJson(json);
Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
static AppFlowyCloudConfiguration defaultConfig() {
return AppFlowyCloudConfiguration(
base_url: '',
ws_base_url: '',
gotrue_url: '',
);
}
}

View File

@ -64,7 +64,7 @@ class FlowyRunner {
// init the app window
const InitAppWindowTask(),
// Init Rust SDK
InitRustSDKTask(directory: applicationDataDirectory),
InitRustSDKTask(customApplicationPath: applicationDataDirectory),
// Load Plugins, like document, grid ...
const PluginLoadTask(),

View File

@ -3,6 +3,7 @@ import 'dart:io';
import 'package:appflowy/env/backend_env.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/user/application/auth/device_id.dart';
import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
@ -11,29 +12,39 @@ import '../startup.dart';
class InitRustSDKTask extends LaunchTask {
const InitRustSDKTask({
this.directory,
this.customApplicationPath,
});
// Customize the RustSDK initialization path
final Directory? directory;
final Directory? customApplicationPath;
@override
LaunchTaskType get type => LaunchTaskType.dataProcessing;
@override
Future<void> initialize(LaunchContext context) async {
final dir = directory ?? await appFlowyApplicationDataDirectory();
final applicationPath = await appFlowyApplicationDataDirectory();
final dir = customApplicationPath ?? applicationPath;
final deviceId = await getDeviceId();
// Pass the environment variables to the Rust SDK
final env = getAppFlowyEnv();
await context.getIt<FlowySDK>().init(dir, jsonEncode(env.toJson()));
final env = _getAppFlowyConfiguration(
dir.path,
applicationPath.path,
deviceId,
);
await context.getIt<FlowySDK>().init(jsonEncode(env.toJson()));
}
@override
Future<void> dispose() async {}
}
AppFlowyEnv getAppFlowyEnv() {
AppFlowyConfiguration _getAppFlowyConfiguration(
String customAppPath,
String originAppPath,
String deviceId,
) {
if (isCloudEnabled) {
final supabaseConfig = SupabaseConfiguration(
url: Env.supabaseUrl,
@ -46,21 +57,24 @@ AppFlowyEnv getAppFlowyEnv() {
gotrue_url: Env.afCloudGoTrueUrl,
);
return AppFlowyEnv(
return AppFlowyConfiguration(
custom_app_path: customAppPath,
origin_app_path: originAppPath,
device_id: deviceId,
cloud_type: Env.cloudType,
supabase_config: supabaseConfig,
appflowy_cloud_config: appflowyCloudConfig,
);
} else {
// Use the default configuration if the cloud feature is disabled
final supabaseConfig = SupabaseConfiguration(url: '', anon_key: '');
final appflowyCloudConfig = AppFlowyCloudConfiguration(
base_url: '',
ws_base_url: '',
gotrue_url: '',
);
final supabaseConfig = SupabaseConfiguration.defaultConfig();
final appflowyCloudConfig = AppFlowyCloudConfiguration.defaultConfig();
return AppFlowyEnv(
return AppFlowyConfiguration(
custom_app_path: customAppPath,
origin_app_path: originAppPath,
device_id: deviceId,
// 0 means the cloud type is local
cloud_type: 0,
supabase_config: supabaseConfig,
appflowy_cloud_config: appflowyCloudConfig,

View File

@ -254,12 +254,44 @@ class _FolderCard extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText.regular(
title,
fontSize: FontSizes.s14,
fontFamily: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
).fontFamily,
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
FlowyText.regular(
title,
fontSize: FontSizes.s14,
fontFamily: GoogleFonts.poppins(
fontWeight: FontWeight.w500,
).fontFamily,
),
Tooltip(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(6),
),
preferBelow: false,
richMessage: WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: Container(
color: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.all(10),
constraints: const BoxConstraints(maxWidth: 450),
child: FlowyText(
LocaleKeys.settings_menu_customPathPrompt.tr(),
maxLines: null,
),
),
),
child: const FlowyIconButton(
icon: Icon(
Icons.warning_amber_rounded,
size: 20,
color: Colors.orangeAccent,
),
),
),
],
),
const VSpace(4),
FlowyText.regular(

View File

@ -44,14 +44,28 @@ class SettingsFileLocationCustomizerState
child: CircularProgressIndicator(),
),
didReceivedPath: (path) {
return Row(
mainAxisSize: MainAxisSize.min,
return Column(
children: [
// display file paths.
_path(path),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// display file paths.
_path(path),
// display the icons
_buttons(path),
// display the icons
_buttons(path),
],
),
const VSpace(10),
IntrinsicHeight(
child: Opacity(
opacity: 0.6,
child: FlowyText.medium(
LocaleKeys.settings_menu_customPathPrompt.tr(),
maxLines: 13,
),
),
),
],
);
},

View File

@ -1,5 +1,4 @@
export 'package:async/async.dart';
import 'dart:io';
import 'dart:async';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:flutter/services.dart';
@ -27,12 +26,11 @@ class FlowySDK {
void dispose() {}
Future<void> init(Directory sdkDir, String env) async {
Future<void> init(String configuration) async {
final port = RustStreamReceiver.shared.port;
ffi.set_stream_port(port);
ffi.store_dart_post_cobject(NativeApi.postCObject);
ffi.set_env(env.toNativeUtf8());
ffi.init_sdk(sdkDir.path.toNativeUtf8());
ffi.init_sdk(configuration.toNativeUtf8());
}
}

View File

@ -20,8 +20,7 @@ DynamicLibrary _open() {
return DynamicLibrary.open('${prefix}/libdart_ffi.so');
if (Platform.isMacOS)
return DynamicLibrary.open('${prefix}/libdart_ffi.dylib');
if (Platform.isIOS)
return DynamicLibrary.open('${prefix}/libdart_ffi.a');
if (Platform.isIOS) return DynamicLibrary.open('${prefix}/libdart_ffi.a');
if (Platform.isWindows)
return DynamicLibrary.open('${prefix}/dart_ffi.dll');
} else {
@ -78,9 +77,9 @@ typedef _invoke_sync_Dart = Pointer<Uint8> Function(
/// C function `init_sdk`.
int init_sdk(
Pointer<ffi.Utf8> path,
Pointer<ffi.Utf8> data,
) {
return _init_sdk(path);
return _init_sdk(data);
}
final _init_sdk_Dart _init_sdk =

View File

@ -3,7 +3,7 @@
#include <stdint.h>
#include <stdlib.h>
int64_t init_sdk(char *path);
int64_t init_sdk(char *data);
void async_event(int64_t port, const uint8_t *input, uintptr_t len);

View File

@ -164,6 +164,7 @@ dependencies = [
"tauri-build",
"tauri-utils",
"tracing",
"uuid",
]
[[package]]
@ -369,6 +370,12 @@ version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "bincode"
version = "1.3.3"
@ -580,6 +587,16 @@ dependencies = [
"serde",
]
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
@ -863,7 +880,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -883,7 +900,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -913,7 +930,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"proc-macro2",
"quote",
@ -925,7 +942,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"collab",
@ -945,7 +962,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"bytes",
@ -959,7 +976,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"chrono",
@ -1001,7 +1018,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -1023,7 +1040,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -1050,7 +1067,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"collab",
@ -1106,6 +1123,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.4.0"
@ -1813,12 +1836,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "1.9.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "fdeflate"
@ -2114,7 +2134,7 @@ dependencies = [
"anyhow",
"base64 0.21.5",
"hmac",
"pbkdf2",
"pbkdf2 0.12.2",
"rand 0.8.5",
"sha2",
]
@ -2251,6 +2271,7 @@ version = "0.1.0"
dependencies = [
"flowy-error",
"serde",
"serde_repr",
]
[[package]]
@ -2964,12 +2985,6 @@ dependencies = [
"libc",
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]]
name = "hex"
version = "0.4.3"
@ -3277,17 +3292,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.48.0",
]
[[package]]
name = "ipnet"
version = "2.8.0"
@ -3485,7 +3489,10 @@ dependencies = [
"md5",
"pin-project",
"rand 0.8.5",
"tempfile",
"tokio",
"walkdir",
"zip",
]
[[package]]
@ -3590,9 +3597,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.3.8"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "lock_api"
@ -3975,7 +3982,7 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
"hermit-abi 0.2.6",
"hermit-abi",
"libc",
]
@ -4207,6 +4214,17 @@ dependencies = [
"regex",
]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "paste"
version = "1.0.14"
@ -4219,6 +4237,18 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
name = "pbkdf2"
version = "0.12.2"
@ -5034,6 +5064,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@ -5297,13 +5336,12 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.20"
version = "0.38.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0"
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
dependencies = [
"bitflags 1.3.2",
"bitflags 2.4.0",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.48.0",
@ -6420,14 +6458,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.6.0"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
"autocfg",
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.48.0",
]
@ -7723,6 +7760,45 @@ dependencies = [
"thiserror",
]
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2 0.11.0",
"sha1",
"time",
"zstd",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.8+zstd.1.5.5"

View File

@ -41,6 +41,7 @@ tracing.workspace = true
lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = ["use_serde"] }
flowy-core = { path = "../../rust-lib/flowy-core", features = ["rev-sqlite", "ts"] }
flowy-notification = { path = "../../rust-lib/flowy-notification", features = ["ts"] }
uuid = "1.5.0"
[features]
# by default Tauri runs in production mode
@ -66,14 +67,14 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b57
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }

View File

@ -7,12 +7,22 @@ pub fn init_flowy_core() -> AppFlowyCore {
let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap();
if cfg!(debug_assertions) {
data_path.push("dev");
data_path.push("data_dev");
} else {
data_path.push("data");
}
data_path.push("data");
let custom_application_path = data_path.to_str().unwrap().to_string();
let application_path = data_path.to_str().unwrap().to_string();
let device_id = uuid::Uuid::new_v4().to_string();
std::env::set_var("RUST_LOG", "trace");
let config = AppFlowyCoreConfig::new(data_path.to_str().unwrap(), DEFAULT_NAME.to_string())
.log_filter("trace", vec!["appflowy_tauri".to_string()]);
let config = AppFlowyCoreConfig::new(
custom_application_path,
application_path,
device_id,
DEFAULT_NAME.to_string(),
)
.log_filter("trace", vec!["appflowy_tauri".to_string()]);
AppFlowyCore::new(config)
}

View File

@ -272,7 +272,8 @@
"inputTextFieldHint": "Your secret",
"historicalUserList": "User login history",
"historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button",
"openHistoricalUser": "Click to open the anonymous account"
"openHistoricalUser": "Click to open the anonymous account",
"customPathPrompt": "Storing the AppFlowy data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption"
},
"notifications": {
"enableNotifications": {

View File

@ -730,7 +730,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -750,7 +750,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -780,7 +780,7 @@ dependencies = [
[[package]]
name = "collab-derive"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"proc-macro2",
"quote",
@ -792,7 +792,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"collab",
@ -812,7 +812,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"bytes",
@ -826,7 +826,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"chrono",
@ -868,7 +868,7 @@ dependencies = [
[[package]]
name = "collab-persistence"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -890,7 +890,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"async-trait",
@ -917,7 +917,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=b8097fa891bdbb5826d6e480460c50ab66d26881#b8097fa891bdbb5826d6e480460c50ab66d26881"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a4f8a08544f6b113fb7b26a0c953e1c1979cf22c#a4f8a08544f6b113fb7b26a0c953e1c1979cf22c"
dependencies = [
"anyhow",
"collab",
@ -1150,7 +1150,7 @@ dependencies = [
"cssparser-macros",
"dtoa-short",
"itoa",
"phf 0.8.0",
"phf 0.11.2",
"smallvec",
]
@ -2079,6 +2079,7 @@ version = "0.1.0"
dependencies = [
"flowy-error",
"serde",
"serde_repr",
]
[[package]]
@ -2995,7 +2996,10 @@ dependencies = [
"md5",
"pin-project",
"rand 0.8.5",
"tempfile",
"tokio",
"walkdir",
"zip",
]
[[package]]
@ -3091,9 +3095,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.5"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "lock_api"
@ -3658,7 +3662,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_macros",
"phf_macros 0.8.0",
"phf_shared 0.8.0",
"proc-macro-hack",
]
@ -3678,6 +3682,7 @@ version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros 0.11.2",
"phf_shared 0.11.2",
]
@ -3745,6 +3750,19 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.31",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
@ -3948,7 +3966,7 @@ checksum = "8bdf592881d821b83d471f8af290226c8d51402259e9bb5be7f9f8bdebbb11ac"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.10.5",
"itertools 0.11.0",
"log",
"multimap",
"once_cell",
@ -3969,7 +3987,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "265baba7fabd416cf5078179f7d2cbeca4ce7a9041111900675ea7c4cb8a4c32"
dependencies = [
"anyhow",
"itertools 0.10.5",
"itertools 0.11.0",
"proc-macro2",
"quote",
"syn 2.0.31",
@ -4348,6 +4366,15 @@ dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags 1.3.2",
]
[[package]]
name = "redox_users"
version = "0.4.3"
@ -4621,9 +4648,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.11"
version = "0.38.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453"
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
dependencies = [
"bitflags 2.4.0",
"errno",
@ -5322,13 +5349,13 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.8.0"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"redox_syscall 0.4.1",
"rustix",
"windows-sys",
]

View File

@ -109,11 +109,11 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "b57
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "b8097fa891bdbb5826d6e480460c50ab66d26881" }
collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }
collab-persistence = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a4f8a08544f6b113fb7b26a0c953e1c1979cf22c" }

View File

@ -8,7 +8,7 @@ use collab::preclude::{CollabBuilder, CollabPlugin};
use collab_entity::{CollabObject, CollabType};
use collab_persistence::kv::rocks_kv::RocksCollabDB;
use collab_plugins::cloud_storage::network_state::{CollabNetworkReachability, CollabNetworkState};
use collab_plugins::local_storage::rocksdb::RocksdbDiskPlugin;
use collab_plugins::local_storage::rocksdb::{RocksdbBackup, RocksdbDiskPlugin};
use collab_plugins::local_storage::CollabPersistenceConfig;
use collab_plugins::snapshot::{CollabSnapshotPlugin, SnapshotPersistence};
use parking_lot::{Mutex, RwLock};
@ -68,17 +68,19 @@ pub struct AppFlowyCollabBuilder {
workspace_id: RwLock<Option<String>>,
cloud_storage: tokio::sync::RwLock<Arc<dyn CollabStorageProvider>>,
snapshot_persistence: Mutex<Option<Arc<dyn SnapshotPersistence>>>,
device_id: Mutex<String>,
rocksdb_backup: Mutex<Option<Arc<dyn RocksdbBackup>>>,
device_id: String,
}
impl AppFlowyCollabBuilder {
pub fn new<T: CollabStorageProvider>(storage_provider: T) -> Self {
pub fn new<T: CollabStorageProvider>(storage_provider: T, device_id: String) -> Self {
Self {
network_reachability: CollabNetworkReachability::new(),
workspace_id: Default::default(),
cloud_storage: tokio::sync::RwLock::new(Arc::new(storage_provider)),
snapshot_persistence: Default::default(),
device_id: Default::default(),
rocksdb_backup: Default::default(),
device_id,
}
}
@ -86,12 +88,12 @@ impl AppFlowyCollabBuilder {
*self.snapshot_persistence.lock() = Some(snapshot_persistence);
}
pub fn initialize(&self, workspace_id: String) {
*self.workspace_id.write() = Some(workspace_id);
pub fn set_rocksdb_backup(&self, rocksdb_backup: Arc<dyn RocksdbBackup>) {
*self.rocksdb_backup.lock() = Some(rocksdb_backup);
}
pub fn set_sync_device(&self, device_id: String) {
*self.device_id.lock() = device_id;
pub fn initialize(&self, workspace_id: String) {
*self.workspace_id.write() = Some(workspace_id);
}
pub fn update_network(&self, reachable: bool) {
@ -120,7 +122,7 @@ impl AppFlowyCollabBuilder {
object_id.to_string(),
collab_type,
workspace_id,
self.device_id.lock().clone(),
self.device_id.clone(),
))
}
@ -146,15 +148,9 @@ impl AppFlowyCollabBuilder {
raw_data: CollabRawData,
collab_db: Weak<RocksCollabDB>,
) -> Result<Arc<MutexCollab>, Error> {
let config = CollabPersistenceConfig::default();
self
.build_with_config(
uid,
object_id,
object_type,
collab_db,
raw_data,
&CollabPersistenceConfig::default(),
)
.build_with_config(uid, object_id, object_type, collab_db, raw_data, &config)
.await
}
@ -188,8 +184,9 @@ impl AppFlowyCollabBuilder {
uid,
collab_db.clone(),
config.clone(),
self.rocksdb_backup.lock().clone(),
))
.with_device_id(self.device_id.lock().clone())
.with_device_id(self.device_id.clone())
.build()?,
);
{

View File

@ -3,7 +3,7 @@
#include <stdint.h>
#include <stdlib.h>
int64_t init_sdk(char *path);
int64_t init_sdk(char *data);
void async_event(int64_t port, const uint8_t *input, uintptr_t len);

View File

@ -1,63 +1,37 @@
use serde::Deserialize;
use serde_repr::Deserialize_repr;
use flowy_server_config::af_cloud_config::AFCloudConfiguration;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_server_config::AuthenticatorType;
#[derive(Deserialize, Debug)]
pub struct AppFlowyEnv {
cloud_type: CloudType,
supabase_config: SupabaseConfiguration,
appflowy_cloud_config: AFCloudConfiguration,
pub struct AppFlowyDartConfiguration {
/// This path will be used to store the user data
pub custom_app_path: String,
pub origin_app_path: String,
pub device_id: String,
pub cloud_type: AuthenticatorType,
pub(crate) supabase_config: SupabaseConfiguration,
pub(crate) appflowy_cloud_config: AFCloudConfiguration,
}
const CLOUT_TYPE_STR: &str = "APPFLOWY_CLOUD_ENV_CLOUD_TYPE";
#[derive(Deserialize_repr, Debug, Clone)]
#[repr(u8)]
pub enum CloudType {
Local = 0,
Supabase = 1,
AppFlowyCloud = 2,
}
impl CloudType {
fn write_env(&self) {
let s = self.clone() as u8;
std::env::set_var(CLOUT_TYPE_STR, s.to_string());
impl AppFlowyDartConfiguration {
pub fn from_str(s: &str) -> Self {
serde_json::from_str::<AppFlowyDartConfiguration>(s).unwrap()
}
#[allow(dead_code)]
fn from_str(s: &str) -> Self {
match s {
"0" => CloudType::Local,
"1" => CloudType::Supabase,
"2" => CloudType::AppFlowyCloud,
_ => CloudType::Local,
}
}
#[allow(dead_code)]
pub fn from_env() -> Self {
let cloud_type_str = std::env::var(CLOUT_TYPE_STR).unwrap_or_default();
CloudType::from_str(&cloud_type_str)
}
}
impl AppFlowyEnv {
/// Parse the environment variable from the frontend application. The frontend will
/// pass the environment variable as a json string after launching.
pub fn write_env_from(env_str: &str) {
if let Ok(env) = serde_json::from_str::<AppFlowyEnv>(env_str) {
let _ = env.cloud_type.write_env();
let is_valid = env.appflowy_cloud_config.write_env().is_ok();
// Note on Configuration Priority:
// If both Supabase config and AppFlowy cloud config are provided in the '.env' file,
// the AppFlowy cloud config will be prioritized and the Supabase config ignored.
// Ensure only one of these configurations is active at any given time.
if !is_valid {
let _ = env.supabase_config.write_env();
}
let configuration = Self::from_str(env_str);
configuration.cloud_type.write_env();
let is_valid = configuration.appflowy_cloud_config.write_env().is_ok();
// Note on Configuration Priority:
// If both Supabase config and AppFlowy cloud config are provided in the '.env' file,
// the AppFlowy cloud config will be prioritized and the Supabase config ignored.
// Ensure only one of these configurations is active at any given time.
if !is_valid {
let _ = configuration.supabase_config.write_env();
}
}
}

View File

@ -13,7 +13,7 @@ use flowy_notification::{register_notification_sender, unregister_all_notificati
use lib_dispatch::prelude::ToBytes;
use lib_dispatch::prelude::*;
use crate::env_serde::AppFlowyEnv;
use crate::env_serde::AppFlowyDartConfiguration;
use crate::notification::DartNotificationSender;
use crate::{
c::{extend_front_four_bytes_into_bytes, forget_rust},
@ -49,13 +49,29 @@ unsafe impl Sync for MutexAppFlowyCore {}
unsafe impl Send for MutexAppFlowyCore {}
#[no_mangle]
pub extern "C" fn init_sdk(path: *mut c_char) -> i64 {
let c_str: &CStr = unsafe { CStr::from_ptr(path) };
let path: &str = c_str.to_str().unwrap();
pub extern "C" fn init_sdk(data: *mut c_char) -> i64 {
let c_str = unsafe { CStr::from_ptr(data) };
let serde_str = c_str.to_str().unwrap();
let configuration = AppFlowyDartConfiguration::from_str(serde_str);
configuration.cloud_type.write_env();
let is_valid = configuration.appflowy_cloud_config.write_env().is_ok();
// Note on Configuration Priority:
// If both Supabase config and AppFlowy cloud config are provided in the '.env' file,
// the AppFlowy cloud config will be prioritized and the Supabase config ignored.
// Ensure only one of these configurations is active at any given time.
if !is_valid {
let _ = configuration.supabase_config.write_env();
}
let log_crates = vec!["flowy-ffi".to_string()];
let config =
AppFlowyCoreConfig::new(path, DEFAULT_NAME.to_string()).log_filter("info", log_crates);
let config = AppFlowyCoreConfig::new(
configuration.custom_app_path,
configuration.origin_app_path,
configuration.device_id,
DEFAULT_NAME.to_string(),
)
.log_filter("info", log_crates);
*APPFLOWY_CORE.0.lock() = Some(AppFlowyCore::new(config));
0
}
@ -163,5 +179,5 @@ pub extern "C" fn backend_log(level: i64, data: *const c_char) {
pub extern "C" fn set_env(data: *const c_char) {
let c_str = unsafe { CStr::from_ptr(data) };
let serde_str = c_str.to_str().unwrap();
AppFlowyEnv::write_env_from(serde_str);
AppFlowyDartConfiguration::write_env_from(serde_str);
}

View File

@ -34,8 +34,10 @@ impl EventIntegrationTest {
std::fs::create_dir_all(&temp_dir).unwrap();
Self::new_with_user_data_path(temp_dir, nanoid!(6)).await
}
pub async fn new_with_user_data_path(path: PathBuf, name: String) -> Self {
let config = AppFlowyCoreConfig::new(path.to_str().unwrap(), name).log_filter(
pub async fn new_with_user_data_path(path_buf: PathBuf, name: String) -> Self {
let path = path_buf.to_str().unwrap().to_string();
let device_id = uuid::Uuid::new_v4().to_string();
let config = AppFlowyCoreConfig::new(path.clone(), path, device_id, name).log_filter(
"trace",
vec![
"flowy_test".to_string(),
@ -53,7 +55,7 @@ impl EventIntegrationTest {
inner,
auth_type,
notification_sender,
cleaner: Arc::new(Cleaner(path)),
cleaner: Arc::new(Cleaner(path_buf)),
}
}
}

View File

@ -787,7 +787,7 @@ async fn hide_group_event_test() {
let groups = test.get_groups(&board_view.id).await;
assert_eq!(groups.len(), 4);
assert_eq!(groups[0].is_visible, false);
assert!(!groups[0].is_visible);
}
// Update the database layout type from grid to board

View File

@ -0,0 +1,20 @@
use event_integration::EventIntegrationTest;
use flowy_core::DEFAULT_NAME;
use crate::util::unzip_history_user_db;
#[tokio::test]
async fn migrate_historical_empty_document_test() {
let (cleaner, user_db_path) = unzip_history_user_db(
"./tests/user/migration_test/history_user_db",
"038_collab_db_corrupt_restore",
)
.unwrap();
let test =
EventIntegrationTest::new_with_user_data_path(user_db_path, DEFAULT_NAME.to_string()).await;
let views = test.get_all_workspace_views().await;
assert_eq!(views.len(), 1);
drop(cleaner);
}

View File

@ -1,2 +1,4 @@
mod document_test;
mod version_test;
mod collab_db_restore;

View File

@ -7,16 +7,22 @@ use tracing::{error, info};
use flowy_server_config::af_cloud_config::AFCloudConfiguration;
use flowy_server_config::supabase_config::SupabaseConfiguration;
use flowy_user::manager::URL_SAFE_ENGINE;
use lib_infra::file_util::copy_dir_recursive;
use crate::integrate::log::create_log_filter;
use crate::integrate::util::copy_dir_recursive;
#[derive(Clone)]
pub struct AppFlowyCoreConfig {
/// Different `AppFlowyCoreConfig` instance should have different name
pub(crate) name: String,
/// Panics if the `root` path is not existing
pub(crate) device_id: String,
/// Used to store the user data
pub storage_path: String,
/// Origin application path is the path of the application binary. By default, the
/// storage_path is the same as the origin_application_path. However, when the user
/// choose a custom path for the user data, the storage_path will be different from
/// the origin_application_path.
pub application_path: String,
pub(crate) log_filter: String,
cloud_config: Option<AFCloudConfiguration>,
}
@ -25,6 +31,7 @@ impl fmt::Debug for AppFlowyCoreConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut debug = f.debug_struct("AppFlowy Configuration");
debug.field("storage_path", &self.storage_path);
debug.field("application_path", &self.application_path);
if let Some(config) = &self.cloud_config {
debug.field("base_url", &config.base_url);
debug.field("ws_url", &config.ws_base_url);
@ -36,7 +43,7 @@ impl fmt::Debug for AppFlowyCoreConfig {
fn migrate_local_version_data_folder(root: &str, url: &str) -> String {
// Isolate the user data folder by using the base url of AppFlowy cloud. This is to avoid
// the user data folder being shared by different AppFlowy cloud.
let server_base64 = URL_SAFE_ENGINE.encode(&url);
let server_base64 = URL_SAFE_ENGINE.encode(url);
let storage_path = format!("{}_{}", root, server_base64);
// Copy the user data folder from the root path to the isolated path
@ -44,7 +51,7 @@ fn migrate_local_version_data_folder(root: &str, url: &str) -> String {
if !Path::new(&storage_path).exists() && Path::new(root).exists() {
info!("Copy dir from {} to {}", root, storage_path);
let src = Path::new(root);
match copy_dir_recursive(&src, Path::new(&storage_path)) {
match copy_dir_recursive(src, Path::new(&storage_path)) {
Ok(_) => storage_path,
Err(err) => {
// when the copy dir failed, use the root path as the storage path
@ -58,22 +65,29 @@ fn migrate_local_version_data_folder(root: &str, url: &str) -> String {
}
impl AppFlowyCoreConfig {
pub fn new(root: &str, name: String) -> Self {
pub fn new(
custom_application_path: String,
application_path: String,
device_id: String,
name: String,
) -> Self {
let cloud_config = AFCloudConfiguration::from_env().ok();
let storage_path = match &cloud_config {
None => {
let supabase_config = SupabaseConfiguration::from_env().ok();
match &supabase_config {
None => root.to_string(),
Some(config) => migrate_local_version_data_folder(root, &config.url),
None => custom_application_path,
Some(config) => migrate_local_version_data_folder(&custom_application_path, &config.url),
}
},
Some(config) => migrate_local_version_data_folder(root, &config.base_url),
Some(config) => migrate_local_version_data_folder(&custom_application_path, &config.base_url),
};
AppFlowyCoreConfig {
name,
storage_path,
application_path,
device_id,
log_filter: create_log_filter("info".to_owned(), vec![]),
cloud_config,
}

View File

@ -0,0 +1,84 @@
use std::sync::{Arc, Weak};
use collab::core::collab_plugin::EncodedCollabV1;
use collab_plugins::local_storage::rocksdb::RocksdbBackup;
use diesel::SqliteConnection;
use flowy_error::FlowyError;
use flowy_sqlite::{prelude::*, schema::rocksdb_backup, schema::rocksdb_backup::dsl};
use flowy_user::manager::UserManager;
use lib_infra::util::timestamp;
pub struct RocksdbBackupImpl(pub Weak<UserManager>);
impl RocksdbBackupImpl {
fn get_pool(&self, uid: i64) -> Result<Arc<ConnectionPool>, FlowyError> {
self
.0
.upgrade()
.ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))?
.db_pool(uid)
}
}
impl RocksdbBackup for RocksdbBackupImpl {
fn save_doc(
&self,
uid: i64,
object_id: &str,
data: EncodedCollabV1,
) -> Result<(), anyhow::Error> {
let row = RocksdbBackupRow {
object_id: object_id.to_string(),
timestamp: timestamp(),
data: data.encode_to_bytes().unwrap_or_default().to_vec(),
};
self
.get_pool(uid)
.map(|pool| RocksdbBackupTableSql::create(row, &*pool.get()?))??;
Ok(())
}
fn get_doc(&self, uid: i64, object_id: &str) -> Result<EncodedCollabV1, anyhow::Error> {
let sql = dsl::rocksdb_backup
.filter(dsl::object_id.eq(object_id))
.into_boxed();
let pool = self.get_pool(uid)?;
let row = pool
.get()
.map(|conn| sql.first::<RocksdbBackupRow>(&*conn))??;
Ok(EncodedCollabV1::decode_from_bytes(&row.data)?)
}
}
#[derive(PartialEq, Clone, Debug, Queryable, Identifiable, Insertable, Associations)]
#[table_name = "rocksdb_backup"]
#[primary_key(object_id)]
struct RocksdbBackupRow {
object_id: String,
timestamp: i64,
data: Vec<u8>,
}
struct RocksdbBackupTableSql;
impl RocksdbBackupTableSql {
fn create(row: RocksdbBackupRow, conn: &SqliteConnection) -> Result<(), FlowyError> {
let _ = replace_into(dsl::rocksdb_backup)
.values(&row)
.execute(conn)?;
Ok(())
}
#[allow(dead_code)]
fn get_row(object_id: &str, conn: &SqliteConnection) -> Result<RocksdbBackupRow, FlowyError> {
let sql = dsl::rocksdb_backup
.filter(dsl::object_id.eq(object_id))
.into_boxed();
let row = sql.first::<RocksdbBackupRow>(conn)?;
Ok(row)
}
}

View File

@ -2,9 +2,7 @@ use std::sync::Weak;
use diesel::SqliteConnection;
use collab_integrate::{
calculate_snapshot_diff, CollabSnapshot, PersistenceError, SnapshotPersistence,
};
use collab_integrate::{CollabSnapshot, PersistenceError, SnapshotPersistence};
use flowy_error::FlowyError;
use flowy_sqlite::{
insert_or_ignore_into,
@ -49,21 +47,13 @@ impl SnapshotPersistence for SnapshotDBImpl {
.get()
.map_err(|e| PersistenceError::Internal(e.into()))?;
let desc = match CollabSnapshotTableSql::get_latest_snapshot(&object_id, &conn) {
None => Ok("".to_string()),
Some(old_snapshot) => {
calculate_snapshot_diff(uid, &object_id, &old_snapshot.data, &snapshot_data)
},
}
.map_err(|e| PersistenceError::InvalidData(format!("{:?}", e)))?;
// Save the snapshot data to disk
let result = CollabSnapshotTableSql::create(
CollabSnapshotRow {
id: uuid::Uuid::new_v4().to_string(),
object_id: object_id.clone(),
title,
desc,
desc: "".to_string(),
collab_type: "".to_string(),
timestamp: timestamp(),
data: snapshot_data,
@ -137,6 +127,7 @@ impl CollabSnapshotTableSql {
Ok(rows)
}
#[allow(dead_code)]
fn get_latest_snapshot(object_id: &str, conn: &SqliteConnection) -> Option<CollabSnapshotRow> {
let sql = dsl::collab_snapshot
.filter(dsl::object_id.eq(object_id))

View File

@ -128,7 +128,7 @@ impl FolderOperationHandler for DocumentFolderOperation {
let manager = self.0.clone();
let view_id = view_id.to_string();
FutureResult::new(async move {
manager.close_document(&view_id)?;
manager.close_document(&view_id).await?;
Ok(())
})
}

View File

@ -8,5 +8,6 @@ mod document_deps;
mod folder_deps;
mod util;
pub mod collab_backup;
mod database_deps;
mod user_deps;

View File

@ -3,4 +3,3 @@ pub(crate) mod log;
pub(crate) mod server;
mod trait_impls;
pub(crate) mod user;
pub(crate) mod util;

View File

@ -6,7 +6,7 @@ use parking_lot::RwLock;
use serde_repr::*;
use flowy_error::{FlowyError, FlowyResult};
use flowy_server::af_cloud::AFCloudServer;
use flowy_server::af_cloud::AppFlowyCloudServer;
use flowy_server::local_server::{LocalServer, LocalServerDB};
use flowy_server::supabase::SupabaseServer;
use flowy_server::{AppFlowyEncryption, AppFlowyServer, EncryptionImpl};
@ -58,7 +58,6 @@ pub struct ServerProvider {
pub(crate) store_preferences: Weak<StorePreferences>,
pub(crate) cache_user_service: RwLock<HashMap<ServerType, Arc<dyn UserCloudService>>>,
pub(crate) device_id: Arc<RwLock<String>>,
pub(crate) enable_sync: RwLock<bool>,
pub(crate) uid: Arc<RwLock<Option<i64>>>,
}
@ -73,7 +72,6 @@ impl ServerProvider {
Self {
config,
server_type: RwLock::new(server_type),
device_id: Arc::new(RwLock::new(uuid::Uuid::new_v4().to_string())),
providers: RwLock::new(HashMap::new()),
enable_sync: RwLock::new(true),
encryption: RwLock::new(Arc::new(encryption)),
@ -115,10 +113,10 @@ impl ServerProvider {
},
ServerType::AFCloud => {
let config = AFCloudConfiguration::from_env()?;
let server = Arc::new(AFCloudServer::new(
let server = Arc::new(AppFlowyCloudServer::new(
config,
*self.enable_sync.read(),
self.device_id.clone(),
self.config.device_id.clone(),
));
Ok::<Arc<dyn AppFlowyServer>, FlowyError>(server)
@ -132,7 +130,7 @@ impl ServerProvider {
uid,
config,
*self.enable_sync.read(),
self.device_id.clone(),
self.config.device_id.clone(),
encryption,
)))
},

View File

@ -110,15 +110,6 @@ impl UserCloudServiceProvider for ServerProvider {
Authenticator::from(server_type)
}
fn set_device_id(&self, device_id: &str) {
if device_id.is_empty() {
tracing::error!("🔴Device id is empty");
return;
}
*self.device_id.write() = device_id.to_string();
}
/// Returns the [UserCloudService] base on the current [ServerType].
/// Creates a new [AppFlowyServer] if it doesn't exist.
fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError> {

View File

@ -1,20 +0,0 @@
use std::fs::{self};
use std::io;
use std::path::Path;
use walkdir::WalkDir;
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
let relative_path = path.strip_prefix(src).unwrap();
let target_path = dst.join(relative_path);
if path.is_dir() {
fs::create_dir_all(&target_path)?;
} else {
fs::copy(path, target_path)?;
}
}
Ok(())
}

View File

@ -15,13 +15,14 @@ use flowy_sqlite::kv::StorePreferences;
use flowy_storage::FileStorageService;
use flowy_task::{TaskDispatcher, TaskRunner};
use flowy_user::event_map::UserCloudServiceProvider;
use flowy_user::manager::{UserManager, UserSessionConfig};
use flowy_user::manager::{UserConfig, UserManager};
use lib_dispatch::prelude::*;
use lib_dispatch::runtime::AFPluginRuntime;
use module::make_plugins;
pub use module::*;
use crate::config::AppFlowyCoreConfig;
use crate::deps_resolve::collab_backup::RocksdbBackupImpl;
use crate::deps_resolve::*;
use crate::integrate::collab_interact::CollabInteractImpl;
use crate::integrate::log::init_log;
@ -108,7 +109,10 @@ impl AppFlowyCore {
) = async {
/// The shared collab builder is used to build the [Collab] instance. The plugins will be loaded
/// on demand based on the [CollabPluginConfig].
let collab_builder = Arc::new(AppFlowyCollabBuilder::new(server_provider.clone()));
let collab_builder = Arc::new(AppFlowyCollabBuilder::new(
server_provider.clone(),
config.device_id.clone(),
));
let user_manager = init_user_manager(
&config,
&store_preference,
@ -119,6 +123,8 @@ impl AppFlowyCore {
collab_builder
.set_snapshot_persistence(Arc::new(SnapshotDBImpl(Arc::downgrade(&user_manager))));
collab_builder.set_rocksdb_backup(Arc::new(RocksdbBackupImpl(Arc::downgrade(&user_manager))));
let database_manager = DatabaseDepsResolver::resolve(
Arc::downgrade(&user_manager),
task_dispatcher.clone(),
@ -169,9 +175,9 @@ impl AppFlowyCore {
document_manager: Arc::downgrade(&document_manager),
};
let cloned_user_session = Arc::downgrade(&user_manager);
if let Some(user_session) = cloned_user_session.upgrade() {
if let Err(err) = user_session
let cloned_user_manager = Arc::downgrade(&user_manager);
if let Some(user_manager) = cloned_user_manager.upgrade() {
if let Err(err) = user_manager
.init(user_status_callback, collab_interact_impl)
.await
{
@ -212,7 +218,12 @@ fn init_user_manager(
user_cloud_service_provider: Arc<dyn UserCloudServiceProvider>,
collab_builder: Weak<AppFlowyCollabBuilder>,
) -> Arc<UserManager> {
let user_config = UserSessionConfig::new(&config.name, &config.storage_path);
let user_config = UserConfig::new(
&config.name,
&config.storage_path,
&config.application_path,
&config.device_id,
);
UserManager::new(
user_config,
user_cloud_service_provider,

View File

@ -117,6 +117,10 @@ impl DatabaseEditor {
///
#[tracing::instrument(level = "debug", skip_all)]
pub async fn close_view_editor(&self, view_id: &str) -> bool {
if let Some(database) = self.database.try_lock() {
let _ = database.flush();
}
self.database_views.close_view(view_id).await
}

View File

@ -18,12 +18,11 @@ use lib_dispatch::prelude::{
use crate::entities::*;
use crate::parser::document_data_parser::DocumentDataParser;
use crate::parser::external::parser::ExternalDataToNestedJSONParser;
use crate::parser::parser_entities::{
ConvertDataToJsonParams, ConvertDataToJsonPayloadPB, ConvertDataToJsonResponsePB,
ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
};
use crate::parser::external::parser::ExternalDataToNestedJSONParser;
use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser};
fn upgrade_document(
@ -69,7 +68,7 @@ pub(crate) async fn close_document_handler(
let manager = upgrade_document(manager)?;
let params: CloseDocumentParams = data.into_inner().try_into()?;
let doc_id = params.document_id;
manager.close_document(&doc_id)?;
manager.close_document(&doc_id).await?;
Ok(())
}

View File

@ -165,9 +165,14 @@ impl DocumentManager {
}
#[instrument(level = "debug", skip(self), err)]
pub fn close_document(&self, doc_id: &str) -> FlowyResult<()> {
// TODO(nathan): remove the document from lru cache. Currently, we don't remove it from the cache.
pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> {
// The lru will pop the least recently used document when the cache is full.
if let Ok(doc) = self.get_document(doc_id).await {
if let Some(doc) = doc.try_lock() {
let _ = doc.flush();
}
}
Ok(())
}

View File

@ -29,7 +29,7 @@ async fn restore_document() {
.get_document_data()
.unwrap();
// close a document
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
assert_eq!(data_b, data);
// restore
@ -43,7 +43,7 @@ async fn restore_document() {
.get_document_data()
.unwrap();
// close a document
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
assert_eq!(data_b, data);
}
@ -85,7 +85,7 @@ async fn document_apply_insert_action() {
document.lock().apply_action(vec![insert_text_action]);
let data_a = document.lock().get_document_data().unwrap();
// close the original document
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
// re-open the document
let data_b = test
@ -96,7 +96,7 @@ async fn document_apply_insert_action() {
.get_document_data()
.unwrap();
// close a document
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
assert_eq!(data_b, data_a);
}
@ -135,7 +135,7 @@ async fn document_apply_update_page_action() {
tracing::trace!("{:?}", &actions);
document.lock().apply_action(actions);
let page_block_old = document.lock().get_block(&data.page_id).unwrap();
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
// re-open the document
let document = test.get_document(&doc_id).await.unwrap();
@ -206,12 +206,12 @@ async fn document_apply_update_action() {
};
document.lock().apply_action(vec![update_text_action]);
// close the original document
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
// re-open the document
let document = test.get_document(&doc_id).await.unwrap();
let block = document.lock().get_block(&text_block_id).unwrap();
assert_eq!(block.data, updated_text_block_data);
// close a document
_ = test.close_document(&doc_id);
_ = test.close_document(&doc_id).await;
}

View File

@ -92,8 +92,8 @@ pub fn db() -> Arc<RocksCollabDB> {
}
pub fn default_collab_builder() -> Arc<AppFlowyCollabBuilder> {
let builder = AppFlowyCollabBuilder::new(DefaultCollabStorageProvider());
builder.set_sync_device(uuid::Uuid::new_v4().to_string());
let builder =
AppFlowyCollabBuilder::new(DefaultCollabStorageProvider(), "fake_device_id".to_string());
builder.initialize(uuid::Uuid::new_v4().to_string());
Arc::new(builder)
}

View File

@ -197,8 +197,8 @@ pub enum ErrorCode {
#[error("Missing auth field")]
MissingAuthField = 65,
#[error("Only one application can access the database")]
MultipleDBInstance = 66,
#[error("Rocksdb IO error")]
RocksdbIOError = 66,
#[error("Document id is empty")]
DocumentIdIsEmpty = 67,
@ -256,6 +256,12 @@ pub enum ErrorCode {
#[error("Not support yet")]
NotSupportYet = 85,
#[error("rocksdb corruption")]
RocksdbCorruption = 86,
#[error("rocksdb internal error")]
RocksdbInternal = 87,
}
impl ErrorCode {

View File

@ -7,4 +7,5 @@ edition = "2021"
[dependencies]
flowy-error = { workspace = true }
serde.workspace = true
serde.workspace = true
serde_repr.workspace = true

View File

@ -1,2 +1,37 @@
use serde_repr::Deserialize_repr;
pub mod af_cloud_config;
pub mod supabase_config;
pub const CLOUT_TYPE_STR: &str = "APPFLOWY_CLOUD_ENV_CLOUD_TYPE";
#[derive(Deserialize_repr, Debug, Clone)]
#[repr(u8)]
pub enum AuthenticatorType {
Local = 0,
Supabase = 1,
AppFlowyCloud = 2,
}
impl AuthenticatorType {
pub fn write_env(&self) {
let s = self.clone() as u8;
std::env::set_var(CLOUT_TYPE_STR, s.to_string());
}
#[allow(dead_code)]
fn from_str(s: &str) -> Self {
match s {
"0" => AuthenticatorType::Local,
"1" => AuthenticatorType::Supabase,
"2" => AuthenticatorType::AppFlowyCloud,
_ => AuthenticatorType::Local,
}
}
#[allow(dead_code)]
pub fn from_env() -> Self {
let cloud_type_str = std::env::var(CLOUT_TYPE_STR).unwrap_or_default();
AuthenticatorType::from_str(&cloud_type_str)
}
}

View File

@ -18,7 +18,7 @@ use crate::af_cloud::impls::user::dto::{
};
use crate::af_cloud::impls::user::util::encryption_type_from_profile;
use crate::af_cloud::{AFCloudClient, AFServer};
use crate::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL};
use crate::supabase::define::USER_SIGN_IN_URL;
pub(crate) struct AFCloudUserAuthServiceImpl<T> {
server: T,
@ -276,7 +276,6 @@ pub async fn user_sign_in_with_url(
user_workspaces,
email: user_profile.email,
token: Some(client.get_token()?),
device_id: params.device_id,
encryption_type,
is_new_user,
updated_at: user_profile.updated_at,
@ -307,9 +306,7 @@ fn oauth_params_from_box_any(any: BoxAny) -> Result<AFCloudOAuthParams, Error> {
.get(USER_SIGN_IN_URL)
.ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing token field"))?
.as_str();
let device_id = map.get(USER_DEVICE_ID).cloned().unwrap_or_default();
Ok(AFCloudOAuthParams {
sign_in_url: sign_in_url.to_string(),
device_id,
})
}

View File

@ -30,24 +30,20 @@ use crate::af_cloud::impls::{
};
use crate::AppFlowyServer;
pub(crate) type AFCloudClient = client_api::Client;
pub(crate) type AFCloudClient = Client;
pub struct AFCloudServer {
pub struct AppFlowyCloudServer {
#[allow(dead_code)]
pub(crate) config: AFCloudConfiguration,
pub(crate) client: Arc<AFCloudClient>,
enable_sync: Arc<AtomicBool>,
#[allow(dead_code)]
device_id: Arc<parking_lot::RwLock<String>>,
device_id: String,
ws_client: Arc<WSClient>,
}
impl AFCloudServer {
pub fn new(
config: AFCloudConfiguration,
enable_sync: bool,
device_id: Arc<parking_lot::RwLock<String>>,
) -> Self {
impl AppFlowyCloudServer {
pub fn new(config: AFCloudConfiguration, enable_sync: bool, device_id: String) -> Self {
let api_client = AFCloudClient::new(&config.base_url, &config.ws_base_url, &config.gotrue_url);
let token_state_rx = api_client.subscribe_token_state();
let enable_sync = Arc::new(AtomicBool::new(enable_sync));
@ -81,7 +77,7 @@ impl AFCloudServer {
}
}
impl AppFlowyServer for AFCloudServer {
impl AppFlowyServer for AppFlowyCloudServer {
fn set_token(&self, token: &str) -> Result<(), Error> {
self
.client
@ -167,7 +163,7 @@ impl AppFlowyServer for AFCloudServer {
WSConnectStateReceiver,
bool,
)>,
anyhow::Error,
Error,
> {
if self.enable_sync.load(Ordering::SeqCst) {
let object_id = _object_id.to_string();
@ -198,13 +194,13 @@ impl AppFlowyServer for AFCloudServer {
/// This function listens to the `token_state_rx` channel for token state updates. Depending on the
/// received state, it either refreshes the WebSocket connection or disconnects from it.
fn spawn_ws_conn(
device_id: &Arc<parking_lot::RwLock<String>>,
device_id: &String,
mut token_state_rx: TokenStateReceiver,
ws_client: &Arc<WSClient>,
api_client: &Arc<Client>,
enable_sync: &Arc<AtomicBool>,
) {
let weak_device_id = Arc::downgrade(device_id);
let cloned_device_id = device_id.to_owned();
let weak_ws_client = Arc::downgrade(ws_client);
let weak_api_client = Arc::downgrade(api_client);
let enable_sync = enable_sync.clone();
@ -217,15 +213,12 @@ fn spawn_ws_conn(
match state {
ConnectState::PingTimeout | ConnectState::Closed => {
// Try to reconnect if the connection is timed out.
if let (Some(api_client), Some(device_id)) =
(weak_api_client.upgrade(), weak_device_id.upgrade())
{
if let Some(api_client) = weak_api_client.upgrade() {
if enable_sync.load(Ordering::SeqCst) {
let device_id = device_id.read().clone();
match api_client.ws_url(&device_id) {
match api_client.ws_url(&cloned_device_id) {
Ok(ws_addr) => {
event!(tracing::Level::INFO, "🟢reconnecting websocket");
let _ = ws_client.connect(ws_addr, &device_id).await;
let _ = ws_client.connect(ws_addr, &cloned_device_id).await;
},
Err(err) => error!("Failed to get ws url: {}", err),
}
@ -245,19 +238,16 @@ fn spawn_ws_conn(
}
});
let weak_device_id = Arc::downgrade(device_id);
let device_id = device_id.to_owned();
let weak_ws_client = Arc::downgrade(ws_client);
let weak_api_client = Arc::downgrade(api_client);
af_spawn(async move {
while let Ok(token_state) = token_state_rx.recv().await {
match token_state {
TokenState::Refresh => {
if let (Some(api_client), Some(ws_client), Some(device_id)) = (
weak_api_client.upgrade(),
weak_ws_client.upgrade(),
weak_device_id.upgrade(),
) {
let device_id = device_id.read().clone();
if let (Some(api_client), Some(ws_client)) =
(weak_api_client.upgrade(), weak_ws_client.upgrade())
{
match api_client.ws_url(&device_id) {
Ok(ws_addr) => {
info!("🟢token state: {:?}, reconnecting websocket", token_state);

View File

@ -45,7 +45,6 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
is_new_user: true,
email: Some(params.email),
token: None,
device_id: params.device_id,
encryption_type: EncryptionType::NoEncryption,
updated_at: timestamp(),
metadata: None,
@ -70,7 +69,6 @@ impl UserCloudService for LocalServerUserAuthServiceImpl {
is_new_user: false,
email: Some(params.email),
token: None,
device_id: params.device_id,
encryption_type: EncryptionType::NoEncryption,
updated_at: timestamp(),
metadata: None,

View File

@ -97,11 +97,7 @@ where
}
// Query the user profile and workspaces
tracing::debug!(
"user uuid: {}, device_id: {}",
params.uuid,
params.device_id
);
tracing::debug!("user uuid: {}", params.uuid,);
let user_profile =
get_user_profile(postgrest.clone(), GetUserProfileParams::Uuid(params.uuid))
.await?
@ -126,7 +122,6 @@ where
is_new_user,
email: Some(user_profile.email),
token: None,
device_id: params.device_id,
encryption_type: EncryptionType::from_sign(&user_profile.encryption_sign),
updated_at: user_profile.updated_at.timestamp(),
metadata: None,
@ -157,7 +152,6 @@ where
is_new_user: false,
email: None,
token: None,
device_id: params.device_id,
encryption_type: EncryptionType::from_sign(&response.encryption_sign),
updated_at: response.updated_at.timestamp(),
metadata: None,
@ -529,11 +523,16 @@ impl RealtimeEventHandler for RealtimeUserHandler {
fn handler_event(&self, event: &RealtimeEvent) {
if let Ok(user_event) = serde_json::from_value::<RealtimeUserEvent>(event.new.clone()) {
let _ = self.0.send(UserUpdate {
uid: user_event.uid,
name: Some(user_event.name),
email: Some(user_event.email),
encryption_sign: user_event.encryption_sign,
let sender = self.0.clone();
tokio::spawn(async move {
let _ = sender
.send(UserUpdate {
uid: user_event.uid,
name: Some(user_event.name),
email: Some(user_event.email),
encryption_sign: user_event.encryption_sign,
})
.await;
});
}
}
@ -541,14 +540,14 @@ impl RealtimeEventHandler for RealtimeUserHandler {
pub struct RealtimeCollabUpdateHandler {
sender_by_oid: Weak<CollabUpdateSenderByOid>,
device_id: Arc<RwLock<String>>,
device_id: String,
encryption: Weak<dyn AppFlowyEncryption>,
}
impl RealtimeCollabUpdateHandler {
pub fn new(
sender_by_oid: Weak<CollabUpdateSenderByOid>,
device_id: Arc<RwLock<String>>,
device_id: String,
encryption: Weak<dyn AppFlowyEncryption>,
) -> Self {
Self {
@ -571,10 +570,10 @@ impl RealtimeEventHandler for RealtimeCollabUpdateHandler {
if let Some(sender) = sender_by_oid.read().get(collab_update.oid.as_str()) {
tracing::trace!(
"current device: {}, event device: {}",
self.device_id.read(),
self.device_id,
collab_update.did.as_str()
);
if *self.device_id.read() != collab_update.did.as_str() {
if self.device_id != collab_update.did {
let encryption_secret = self
.encryption
.upgrade()
@ -623,10 +622,5 @@ fn oauth_params_from_box_any(any: BoxAny) -> Result<SupabaseOAuthParams, Error>
let map: HashMap<String, String> = any.unbox_or_error()?;
let uuid = uuid_from_map(&map)?;
let email = map.get("email").cloned().unwrap_or_default();
let device_id = map.get("device_id").cloned().unwrap_or_default();
Ok(SupabaseOAuthParams {
uuid,
email,
device_id,
})
Ok(SupabaseOAuthParams { uuid, email })
}

View File

@ -61,8 +61,7 @@ pub type CollabUpdateSenderByOid = RwLock<HashMap<String, RemoteUpdateSender>>;
pub struct SupabaseServer {
#[allow(dead_code)]
config: SupabaseConfiguration,
/// did represents as the device id is used to identify the device that is currently using the app.
device_id: Arc<RwLock<String>>,
device_id: String,
uid: Arc<RwLock<Option<i64>>>,
collab_update_sender: Arc<CollabUpdateSenderByOid>,
restful_postgres: Arc<RwLock<Option<Arc<RESTfulPostgresServer>>>>,
@ -75,7 +74,7 @@ impl SupabaseServer {
uid: Arc<RwLock<Option<i64>>>,
config: SupabaseConfiguration,
enable_sync: bool,
device_id: Arc<RwLock<String>>,
device_id: String,
encryption: Weak<dyn AppFlowyEncryption>,
) -> Self {
let collab_update_sender = Default::default();
@ -146,7 +145,7 @@ impl AppFlowyServer for SupabaseServer {
));
// handle the realtime user event.
let user_handler = Box::new(RealtimeUserHandler(user_update_tx.clone()));
let user_handler = Box::new(RealtimeUserHandler(user_update_tx));
let handlers: Vec<Box<dyn RealtimeEventHandler>> = vec![collab_update_handler, user_handler];
Arc::new(SupabaseUserServiceImpl::new(

View File

@ -1,10 +1,9 @@
use std::collections::HashMap;
use std::sync::Arc;
use parking_lot::RwLock;
use uuid::Uuid;
use flowy_server::af_cloud::AFCloudServer;
use flowy_server::af_cloud::AppFlowyCloudServer;
use flowy_server::supabase::define::{USER_DEVICE_ID, USER_SIGN_IN_URL};
use flowy_server_config::af_cloud_config::AFCloudConfiguration;
@ -24,10 +23,9 @@ pub fn get_af_cloud_config() -> Option<AFCloudConfiguration> {
AFCloudConfiguration::from_env().ok()
}
pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AFCloudServer> {
pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc<AppFlowyCloudServer> {
let fake_device_id = uuid::Uuid::new_v4().to_string();
let device_id = Arc::new(RwLock::new(fake_device_id));
Arc::new(AFCloudServer::new(config, true, device_id))
Arc::new(AppFlowyCloudServer::new(config, true, fake_device_id))
}
pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String {
@ -42,7 +40,7 @@ pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguratio
.unwrap();
let action_link = admin_client
.generate_sign_in_action_link(&user_email)
.generate_sign_in_action_link(user_email)
.await
.unwrap();
client.extract_sign_in_url(&action_link).await.unwrap()

View File

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE rocksdb_backup;

View File

@ -0,0 +1,6 @@
-- Your SQL goes here
CREATE TABLE rocksdb_backup (
object_id TEXT NOT NULL PRIMARY KEY,
timestamp BIGINT NOT NULL DEFAULT 0,
data BLOB NOT NULL DEFAULT (x'')
);

View File

@ -12,6 +12,14 @@ diesel::table! {
}
}
diesel::table! {
rocksdb_backup (object_id) {
object_id -> Text,
timestamp -> BigInt,
data -> Binary,
}
}
diesel::table! {
user_data_migration_records (id) {
id -> Integer,
@ -48,6 +56,7 @@ diesel::table! {
diesel::allow_tables_to_appear_in_same_query!(
collab_snapshot,
rocksdb_backup,
user_data_migration_records,
user_table,
user_workspace_table,

View File

@ -16,7 +16,6 @@ pub trait UserAuthResponse {
fn user_name(&self) -> &str;
fn latest_workspace(&self) -> &UserWorkspace;
fn user_workspaces(&self) -> &[UserWorkspace];
fn device_id(&self) -> &str;
fn user_token(&self) -> Option<String>;
fn user_email(&self) -> Option<String>;
fn encryption_type(&self) -> EncryptionType;
@ -30,7 +29,6 @@ pub struct SignInParams {
pub password: String,
pub name: String,
pub auth_type: Authenticator,
pub device_id: String,
}
#[derive(Serialize, Deserialize, Default, Debug)]
@ -51,7 +49,6 @@ pub struct AuthResponse {
pub is_new_user: bool,
pub email: Option<String>,
pub token: Option<String>,
pub device_id: String,
pub encryption_type: EncryptionType,
pub updated_at: i64,
pub metadata: Option<serde_json::Value>,
@ -74,10 +71,6 @@ impl UserAuthResponse for AuthResponse {
&self.user_workspaces
}
fn device_id(&self) -> &str {
&self.device_id
}
fn user_token(&self) -> Option<String> {
self.token.clone()
}
@ -364,12 +357,10 @@ impl From<i32> for Authenticator {
pub struct SupabaseOAuthParams {
pub uuid: Uuid,
pub email: String,
pub device_id: String,
}
pub struct AFCloudOAuthParams {
pub sign_in_url: String,
pub device_id: String,
}
#[derive(Clone, Debug)]

View File

@ -37,7 +37,6 @@ impl TryInto<SignInParams> for SignInPayloadPB {
password: password.0,
name: self.name,
auth_type: self.auth_type.into(),
device_id: self.device_id,
})
}
}

View File

@ -482,7 +482,7 @@ pub async fn open_historical_users_handler(
let manager = upgrade_manager(manager)?;
let auth_type = Authenticator::from(user.auth_type);
manager
.open_historical_user(user.user_id, user.device_id, auth_type)
.open_historical_user(user.user_id, auth_type)
.await?;
Ok(())
}
@ -541,8 +541,8 @@ pub async fn reset_workspace_handler(
"The workspace id is empty",
));
}
let session = manager.get_session()?;
manager.reset_workspace(reset_pb, session.device_id).await?;
let _session = manager.get_session()?;
manager.reset_workspace(reset_pb).await?;
Ok(())
}

View File

@ -246,7 +246,6 @@ pub trait UserCloudServiceProvider: Send + Sync + 'static {
fn set_encrypt_secret(&self, secret: String);
fn set_authenticator(&self, authenticator: Authenticator);
fn get_authenticator(&self) -> Authenticator;
fn set_device_id(&self, device_id: &str);
fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError>;
fn service_url(&self) -> String;
}
@ -275,10 +274,6 @@ where
(**self).get_authenticator()
}
fn set_device_id(&self, device_id: &str) {
(**self).set_device_id(device_id)
}
fn get_user_service(&self) -> Result<Arc<dyn UserCloudService>, FlowyError> {
(**self).get_user_service()
}

View File

@ -1,3 +1,4 @@
use std::fs;
use std::path::PathBuf;
use std::string::ToString;
use std::sync::atomic::{AtomicI64, Ordering};
@ -41,28 +42,42 @@ use crate::services::user_workspace::save_user_workspaces;
use crate::{errors::FlowyError, notification::*};
pub const URL_SAFE_ENGINE: GeneralPurpose = GeneralPurpose::new(&URL_SAFE, PAD);
pub struct UserSessionConfig {
root_dir: String,
pub struct UserConfig {
/// Used to store the user data
storage_path: String,
/// application_path is the path of the application binary. By default, the
/// storage_path is the same as the application_path. However, when the user
/// choose a custom path for the user data, the storage_path will be different from
/// the application_path.
application_path: String,
pub device_id: String,
/// Used as the key of `Session` when saving session information to KV.
session_cache_key: String,
}
impl UserSessionConfig {
impl UserConfig {
/// The `root_dir` represents as the root of the user folders. It must be unique for each
/// users.
pub fn new(name: &str, root_dir: &str) -> Self {
pub fn new(name: &str, storage_path: &str, application_path: &str, device_id: &str) -> Self {
let session_cache_key = format!("{}_session_cache", name);
Self {
root_dir: root_dir.to_owned(),
storage_path: storage_path.to_owned(),
application_path: application_path.to_owned(),
session_cache_key,
device_id: device_id.to_owned(),
}
}
/// Returns bool whether the user choose a custom path for the user data.
pub fn is_custom_storage_path(&self) -> bool {
self.storage_path != self.application_path
}
}
pub struct UserManager {
database: Arc<UserDB>,
user_paths: UserPaths,
session_config: UserSessionConfig,
pub(crate) user_config: UserConfig,
pub(crate) cloud_services: Arc<dyn UserCloudServiceProvider>,
pub(crate) store_preferences: Arc<StorePreferences>,
pub(crate) user_awareness: Arc<Mutex<Option<MutexUserAwareness>>>,
@ -76,14 +91,12 @@ pub struct UserManager {
impl UserManager {
pub fn new(
session_config: UserSessionConfig,
user_config: UserConfig,
cloud_services: Arc<dyn UserCloudServiceProvider>,
store_preferences: Arc<StorePreferences>,
collab_builder: Weak<AppFlowyCollabBuilder>,
) -> Arc<Self> {
let user_paths = UserPaths {
root: session_config.root_dir.clone(),
};
let user_paths = UserPaths::new(user_config.storage_path.clone());
let database = Arc::new(UserDB::new(user_paths.clone()));
let user_status_callback: RwLock<Arc<dyn UserStatusCallback>> =
RwLock::new(Arc::new(DefaultUserStatusCallback));
@ -92,7 +105,7 @@ impl UserManager {
let user_manager = Arc::new(Self {
database,
user_paths,
session_config,
user_config,
cloud_services,
store_preferences,
user_awareness: Arc::new(Default::default()),
@ -183,11 +196,14 @@ impl UserManager {
}
});
}
self.prepare_user(&session).await;
// Do the user data migration if needed
event!(tracing::Level::INFO, "Prepare user data migration");
match (
self.database.get_collab_db(session.user_id),
self
.database
.get_collab_db(session.user_id, &self.user_config.device_id),
self.database.get_pool(session.user_id),
) {
(Ok(collab_db), Ok(sqlite_pool)) => {
@ -209,7 +225,6 @@ impl UserManager {
},
_ => error!("Failed to get collab db or sqlite pool"),
}
self.set_collab_config(&session);
// Init the user awareness
self
.initialize_user_awareness(&session, UserAwarenessDataSource::Local)
@ -221,7 +236,7 @@ impl UserManager {
session.user_id,
&cloud_config,
&session.user_workspace,
&session.device_id,
&self.user_config.device_id,
)
.await
{
@ -242,7 +257,7 @@ impl UserManager {
pub fn get_collab_db(&self, uid: i64) -> Result<Weak<RocksCollabDB>, FlowyError> {
self
.database
.get_collab_db(uid)
.get_collab_db(uid, "")
.map(|collab_db| Arc::downgrade(&collab_db))
}
@ -267,13 +282,14 @@ impl UserManager {
.sign_in(params)
.await?;
let session = Session::from(&response);
self.set_collab_config(&session);
self.prepare_user(&session).await;
let latest_workspace = response.latest_workspace.clone();
let user_profile = UserProfile::from((&response, &authenticator));
self
.save_auth_data(&response, &authenticator, &session)
.await?;
let _ = self
.initialize_user_awareness(&session, UserAwarenessDataSource::Remote)
.await;
@ -282,7 +298,11 @@ impl UserManager {
.user_status_callback
.read()
.await
.did_sign_in(user_profile.uid, &latest_workspace, &session.device_id)
.did_sign_in(
user_profile.uid,
&latest_workspace,
&self.user_config.device_id,
)
.await
{
error!("Failed to call did_sign_in callback: {:?}", e);
@ -373,7 +393,7 @@ impl UserManager {
authenticator: &Authenticator,
) -> FlowyResult<()> {
let new_session = Session::from(&response);
self.set_collab_config(&new_session);
self.prepare_user(&new_session).await;
let user_awareness_source = if response.is_new_user {
UserAwarenessDataSource::Local
@ -415,7 +435,7 @@ impl UserManager {
response.is_new_user,
user_profile,
&new_session.user_workspace,
&new_session.device_id,
&self.user_config.device_id,
)
.await?;
@ -468,6 +488,24 @@ impl UserManager {
Ok(())
}
pub async fn prepare_user(&self, session: &Session) {
self.set_collab_config(session);
// Ensure to backup user data if a cloud drive is used for storage. While using a cloud drive
// for storing user data is not advised due to potential data corruption risks, in scenarios where
// users opt for cloud storage, the application should automatically create a backup of the user
// data. This backup should be in the form of a zip file and stored locally on the user's disk
// for safety and data integrity purposes
if self.user_config.is_custom_storage_path() {
self
.database
.backup_or_restore(session.user_id, &session.user_workspace.id);
} else {
self
.database
.restore_if_need(session.user_id, &session.user_workspace.id);
}
}
/// Fetches the user profile for the given user ID.
pub async fn get_user_profile(&self, uid: i64) -> Result<UserProfile, FlowyError> {
let user: UserProfile = user_table::dsl::user_table
@ -623,7 +661,7 @@ impl UserManager {
match self
.store_preferences
.get_object::<Session>(&self.session_config.session_cache_key)
.get_object::<Session>(&self.user_config.session_cache_key)
{
None => Err(FlowyError::new(
ErrorCode::RecordNotFound,
@ -643,13 +681,13 @@ impl UserManager {
self.current_session.write().take();
self
.store_preferences
.remove(&self.session_config.session_cache_key)
.remove(&self.user_config.session_cache_key)
},
Some(session) => {
self.current_session.write().replace(session.clone());
self
.store_preferences
.set_object(&self.session_config.session_cache_key, session.clone())
.set_object(&self.user_config.session_cache_key, session.clone())
.map_err(internal_error)?;
},
}
@ -695,7 +733,7 @@ impl UserManager {
event!(tracing::Level::DEBUG, "Save new history user: {:?}", uid);
self.add_historical_user(
uid,
response.device_id(),
&self.user_config.device_id,
response.user_name().to_string(),
authenticator,
self.user_dir(uid),
@ -712,9 +750,7 @@ impl UserManager {
fn set_collab_config(&self, session: &Session) {
let collab_builder = self.collab_builder.upgrade().unwrap();
collab_builder.set_sync_device(session.device_id.clone());
collab_builder.initialize(session.user_workspace.id.clone());
self.cloud_services.set_device_id(&session.device_id);
}
async fn handler_user_update(&self, user_update: UserUpdate) -> FlowyResult<()> {
@ -742,13 +778,17 @@ impl UserManager {
old_user: &MigrationUser,
new_user: &MigrationUser,
) -> Result<(), FlowyError> {
let old_collab_db = self.database.get_collab_db(old_user.session.user_id)?;
let new_collab_db = self.database.get_collab_db(new_user.session.user_id)?;
let old_collab_db = self
.database
.get_collab_db(old_user.session.user_id, &self.user_config.device_id)?;
let new_collab_db = self
.database
.get_collab_db(new_user.session.user_id, &self.user_config.device_id)?;
migration_anon_user_on_sign_up(old_user, &old_collab_db, new_user, &new_collab_db)?;
if let Err(err) = sync_user_data_to_cloud(
self.cloud_services.get_user_service()?,
"",
&self.user_config.device_id,
new_user,
&new_collab_db,
)
@ -816,6 +856,9 @@ struct UserPaths {
}
impl UserPaths {
fn new(root: String) -> Self {
Self { root }
}
fn user_dir(&self, uid: i64) -> String {
format!("{}/{}", self.root, uid)
}
@ -831,4 +874,12 @@ impl UserDBPath for UserPaths {
path.push("collab_db");
path
}
fn collab_db_history(&self, uid: i64, create_if_not_exist: bool) -> std::io::Result<PathBuf> {
let path = PathBuf::from(self.user_dir(uid)).join("collab_db_history");
if !path.exists() && create_if_not_exist {
fs::create_dir_all(&path)?;
}
Ok(path)
}
}

View File

@ -1,10 +1,12 @@
use std::path::{Path, PathBuf};
use std::{collections::HashMap, sync::Arc, time::Duration};
use std::{collections::HashMap, fs, io, sync::Arc, time::Duration};
use chrono::Local;
use lazy_static::lazy_static;
use parking_lot::RwLock;
use tracing::{error, event, info, instrument};
use collab_integrate::RocksCollabDB;
use collab_integrate::{PersistenceError, RocksCollabDB, YrsDocAction};
use flowy_error::{ErrorCode, FlowyError};
use flowy_sqlite::schema::user_workspace_table;
use flowy_sqlite::ConnectionPool;
@ -14,6 +16,8 @@ use flowy_sqlite::{
DBConnection, Database, ExpressionMethods,
};
use flowy_user_deps::entities::{UserProfile, UserWorkspace};
use lib_dispatch::prelude::af_spawn;
use lib_infra::file_util::{unzip_and_replace, zip_folder};
use crate::services::user_sql::UserTable;
use crate::services::user_workspace_sql::UserWorkspaceTable;
@ -21,6 +25,7 @@ use crate::services::user_workspace_sql::UserWorkspaceTable;
pub trait UserDBPath: Send + Sync + 'static {
fn user_db_path(&self, uid: i64) -> PathBuf;
fn collab_db_path(&self, uid: i64) -> PathBuf;
fn collab_db_history(&self, uid: i64, create_if_not_exist: bool) -> std::io::Result<PathBuf>;
}
pub struct UserDB {
@ -34,6 +39,58 @@ impl UserDB {
}
}
/// Performs a conditional backup or restoration of the collaboration database (CollabDB) for a specific user.
///
/// This function takes a user ID and conducts the following operations:
///
/// **Backup or Restoration**:
/// - If the CollabDB exists, it tries to open the database:
/// - **Successful Open**: If the database opens successfully, it attempts to back it up.
/// - **Failed Open**: If the database cannot be opened, it indicates a potential issue, and the function
/// attempts to restore the database from the latest backup.
/// - If the CollabDB does not exist, it immediately attempts to restore from the latest backup.
///
pub fn backup_or_restore(&self, uid: i64, workspace_id: &str) {
let collab_db_path = self.paths.collab_db_path(uid);
if let Ok(history_folder) = self.paths.collab_db_history(uid, true) {
if collab_db_path.exists() {
let is_ok = validate_collab_db(&collab_db_path, uid, workspace_id);
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
if is_ok {
// If the database opens successfully, it attempts to back it up in the background.
af_spawn(async move {
let _ = tokio::task::spawn_blocking(move || {
if let Err(err) = zip_backup.backup() {
error!("backup collab db failed, {:?}", err);
}
})
.await;
});
} else if let Err(err) = zip_backup.restore_latest_backup() {
error!("restore collab db failed, {:?}", err);
}
} else {
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
if let Err(err) = zip_backup.restore_latest_backup() {
error!("restore collab db failed, {:?}", err);
}
}
}
}
pub fn restore_if_need(&self, uid: i64, workspace_id: &str) {
if let Ok(history_folder) = self.paths.collab_db_history(uid, false) {
let collab_db_path = self.paths.collab_db_path(uid);
let is_ok = validate_collab_db(&collab_db_path, uid, workspace_id);
let zip_backup = CollabDBZipBackup::new(collab_db_path, history_folder);
if !is_ok {
if let Err(err) = zip_backup.restore_latest_backup() {
error!("restore collab db failed, {:?}", err);
}
}
}
}
/// Close the database connection for the user.
pub(crate) fn close(&self, user_id: i64) -> Result<(), FlowyError> {
if let Some(mut sqlite_dbs) = DB_MAP.try_write_for(Duration::from_millis(300)) {
@ -44,6 +101,7 @@ impl UserDB {
if let Some(mut collab_dbs) = COLLAB_DB_MAP.try_write_for(Duration::from_millis(300)) {
if let Some(db) = collab_dbs.remove(&user_id) {
tracing::trace!("close collab db for user {}", user_id);
let _ = db.flush();
drop(db);
}
}
@ -60,8 +118,17 @@ impl UserDB {
Ok(pool)
}
pub(crate) fn get_collab_db(&self, user_id: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
let collab_db = open_collab_db(self.paths.collab_db_path(user_id), user_id)?;
pub(crate) fn get_collab_db(
&self,
user_id: i64,
_device_id: &str,
) -> Result<Arc<RocksCollabDB>, FlowyError> {
let collab_db = open_collab_db(
self.paths.user_db_path(user_id),
self.paths.collab_db_path(user_id),
self.paths.collab_db_history(user_id, false).ok(),
user_id,
)?;
Ok(collab_db)
}
}
@ -107,18 +174,32 @@ pub fn get_user_workspace(
/// Open a collab db for the user. If the db is already opened, return the opened db.
///
fn open_collab_db(db_path: impl AsRef<Path>, uid: i64) -> Result<Arc<RocksCollabDB>, FlowyError> {
fn open_collab_db(
_collab_backup_db_path: impl AsRef<Path>,
collab_db_path: impl AsRef<Path>,
_collab_db_history: Option<PathBuf>,
uid: i64,
) -> Result<Arc<RocksCollabDB>, FlowyError> {
if let Some(collab_db) = COLLAB_DB_MAP.read().get(&uid) {
return Ok(collab_db.clone());
}
let mut write_guard = COLLAB_DB_MAP.write();
tracing::trace!("open collab db {} at path: {:?}", uid, db_path.as_ref());
let db = match RocksCollabDB::open(db_path) {
let db = match RocksCollabDB::open(&collab_db_path) {
Ok(db) => Ok(db),
Err(err) => {
tracing::error!("open collab db failed, {:?}", err);
Err(FlowyError::new(ErrorCode::MultipleDBInstance, err))
tracing::error!("open collab db error, {:?}", err);
match err {
PersistenceError::RocksdbCorruption(_) => {
// try restore from the backup db
Err(FlowyError::new(ErrorCode::RocksdbCorruption, err))
},
PersistenceError::RocksdbIOError(_) => {
//
Err(FlowyError::new(ErrorCode::RocksdbIOError, err))
},
_ => Err(FlowyError::new(ErrorCode::RocksdbInternal, err)),
}
},
}?;
@ -132,3 +213,140 @@ lazy_static! {
static ref DB_MAP: RwLock<HashMap<i64, Database>> = RwLock::new(HashMap::new());
static ref COLLAB_DB_MAP: RwLock<HashMap<i64, Arc<RocksCollabDB>>> = RwLock::new(HashMap::new());
}
pub struct CollabDBZipBackup {
collab_db_path: PathBuf,
history_folder: PathBuf,
}
impl CollabDBZipBackup {
fn new(collab_db_path: PathBuf, history_folder: PathBuf) -> Self {
Self {
collab_db_path,
history_folder,
}
}
#[instrument(name = "backup_collab_db", skip_all, err)]
pub fn backup(&self) -> io::Result<()> {
let today_zip_file = self
.history_folder
.join(format!("collab_db_{}.zip", today_zip_timestamp()));
// Remove today's existing zip file if it exists
if !today_zip_file.exists() {
// Create a backup for today
event!(
tracing::Level::INFO,
"Backup collab db to {:?}",
today_zip_file
);
zip_folder(&self.collab_db_path, &today_zip_file)?;
}
// Clean up old backups
self.clean_old_backups()?;
Ok(())
}
#[instrument(skip_all, err)]
pub fn restore_latest_backup(&self) -> io::Result<()> {
let mut latest_zip: Option<(String, PathBuf)> = None;
for entry in fs::read_dir(&self.history_folder)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("zip") {
if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
if let Some(timestamp_str) = file_name.strip_prefix("collab_db_") {
match latest_zip {
Some((latest_timestamp, _)) if timestamp_str > latest_timestamp.as_str() => {
latest_zip = Some((timestamp_str.to_string(), path));
},
None => latest_zip = Some((timestamp_str.to_string(), path)),
_ => {},
}
}
}
}
}
let restore_path = latest_zip
.map(|(_, path)| path)
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "No backup folder found"))?;
unzip_and_replace(&restore_path, &self.collab_db_path)?;
info!("Restore collab db from {:?}", restore_path);
Ok(())
}
fn clean_old_backups(&self) -> io::Result<()> {
let mut backups = Vec::new();
let threshold_date = Local::now() - chrono::Duration::days(10);
// Collect all backup files
for entry in fs::read_dir(&self.history_folder)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("zip") {
let filename = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or_default();
let date_str = filename.split('_').last().unwrap_or("");
backups.push((date_str.to_string(), path));
}
}
// Sort backups by date (oldest first)
backups.sort_by(|a, b| a.0.cmp(&b.0));
// Remove backups older than 10 days
let threshold_str = threshold_date.format(zip_time_format()).to_string();
// If there are more than 10 backups, remove the oldest ones
while backups.len() > 10 {
if let Some((date_str, path)) = backups.first() {
if date_str < &threshold_str {
info!("Remove old backup file: {:?}", path);
fs::remove_file(path)?;
backups.remove(0);
} else {
break;
}
}
}
Ok(())
}
}
fn today_zip_timestamp() -> String {
Local::now().format(zip_time_format()).to_string()
}
fn zip_time_format() -> &'static str {
"%Y%m%d"
}
pub(crate) fn validate_collab_db(
collab_db_path: impl AsRef<Path>,
uid: i64,
workspace_id: &str,
) -> bool {
// Attempt to open the collaboration database using the workspace_id. The workspace_id must already
// exist in the collab database. If it does not, it may be indicative of corruption in the collab database
// due to other factors.
let result = RocksCollabDB::open(&collab_db_path).map(|db| {
let read_txn = db.read_txn();
read_txn.is_exist(uid, workspace_id)
});
match result {
Ok(is_ok) => is_ok,
// return false if the error is not related to corruption
Err(err) => !matches!(
err,
PersistenceError::RocksdbCorruption(_) | PersistenceError::RocksdbRepairFail(_)
),
}
}

View File

@ -16,7 +16,6 @@ use crate::migrations::MigrationUser;
#[derive(Debug, Clone, Serialize)]
pub struct Session {
pub user_id: i64,
pub device_id: String,
pub user_workspace: UserWorkspace,
}
@ -36,7 +35,6 @@ impl<'de> Visitor<'de> for SessionVisitor {
// For historical reasons, the session used to contain a workspace_id field.
// This field is no longer used, and is replaced by user_workspace.
let mut workspace_id = None;
let mut device_id = "phantom".to_string();
let mut user_workspace = None;
while let Some(key) = map.next_key::<String>()? {
@ -47,9 +45,6 @@ impl<'de> Visitor<'de> for SessionVisitor {
"workspace_id" => {
workspace_id = Some(map.next_value()?);
},
"device_id" => {
device_id = map.next_value()?;
},
"user_workspace" => {
user_workspace = Some(map.next_value()?);
},
@ -73,7 +68,6 @@ impl<'de> Visitor<'de> for SessionVisitor {
let session = Session {
user_id,
device_id,
user_workspace: user_workspace.ok_or(serde::de::Error::missing_field("user_workspace"))?,
};
@ -97,7 +91,6 @@ where
fn from(value: &T) -> Self {
Self {
user_id: value.user_id(),
device_id: value.device_id().to_string(),
user_workspace: value.latest_workspace().clone(),
}
}

View File

@ -82,12 +82,7 @@ impl UserManager {
/// This function facilitates the re-opening of a user's session from historical tracking.
/// It retrieves the user's workspace and establishes a new session for the user.
///
pub async fn open_historical_user(
&self,
uid: i64,
device_id: String,
auth_type: Authenticator,
) -> FlowyResult<()> {
pub async fn open_historical_user(&self, uid: i64, auth_type: Authenticator) -> FlowyResult<()> {
debug_assert!(auth_type.is_local());
self.update_authenticator(&auth_type).await;
let conn = self.db_connection(uid)?;
@ -97,7 +92,6 @@ impl UserManager {
let user_workspace = UserWorkspace::from(row);
let session = Session {
user_id: uid,
device_id,
user_workspace,
};
self.set_session(Some(session))?;

View File

@ -123,17 +123,13 @@ impl UserManager {
/// Reset the remote workspace using local workspace data. This is useful when a user wishes to
/// open a workspace on a new device that hasn't fully synchronized with the server.
pub async fn reset_workspace(
&self,
reset: ResetWorkspacePB,
device_id: String,
) -> FlowyResult<()> {
pub async fn reset_workspace(&self, reset: ResetWorkspacePB) -> FlowyResult<()> {
let collab_object = CollabObject::new(
reset.uid,
reset.workspace_id.clone(),
CollabType::Folder,
reset.workspace_id.clone(),
device_id,
self.user_config.device_id.clone(),
);
self
.cloud_services

View File

@ -28,7 +28,7 @@ impl Builder {
Builder {
name: name.to_owned(),
env_filter: "Info".to_owned(),
file_appender: tracing_appender::rolling::daily(directory, format!("{}", name)),
file_appender: tracing_appender::rolling::daily(directory, name),
}
}

382
shared-lib/Cargo.lock generated
View File

@ -17,6 +17,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aes"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aho-corasick"
version = "0.7.18"
@ -88,6 +99,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
name = "basic-toml"
version = "0.1.2"
@ -118,6 +135,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
[[package]]
name = "block-buffer"
version = "0.10.4"
@ -143,17 +166,47 @@ version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "bzip2"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8"
dependencies = [
"bzip2-sys",
"libc",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@ -170,7 +223,7 @@ dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets 0.48.0",
"windows-targets",
]
[[package]]
@ -195,6 +248,16 @@ dependencies = [
"phf_codegen",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "cmd_lib"
version = "1.3.0"
@ -245,6 +308,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "constant_time_eq"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@ -260,6 +329,24 @@ dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
dependencies = [
"cfg-if",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -325,6 +412,15 @@ dependencies = [
"parking_lot",
]
[[package]]
name = "deranged"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3"
dependencies = [
"powerfmt",
]
[[package]]
name = "deunicode"
version = "0.4.3"
@ -339,6 +435,7 @@ checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
dependencies = [
"block-buffer",
"crypto-common",
"subtle",
]
[[package]]
@ -380,7 +477,7 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"libc",
"winapi",
]
@ -397,11 +494,18 @@ dependencies = [
[[package]]
name = "fastrand"
version = "1.9.0"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"instant",
"crc32fast",
"miniz_oxide",
]
[[package]]
@ -529,7 +633,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
dependencies = [
"bitflags",
"bitflags 1.3.2",
"ignore",
"walkdir",
]
@ -559,10 +663,13 @@ dependencies = [
]
[[package]]
name = "hermit-abi"
version = "0.3.1"
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest",
]
[[package]]
name = "humansize"
@ -632,23 +739,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497f036ac2fae75c34224648a77802e5dd4e9cfb56f4713ab6b12b7160a0523b"
[[package]]
name = "instant"
version = "0.1.12"
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"cfg-if",
]
[[package]]
name = "io-lifetimes"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.48.0",
"generic-array",
]
[[package]]
@ -666,6 +762,15 @@ version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
[[package]]
name = "jobserver"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.61"
@ -693,7 +798,10 @@ dependencies = [
"md5",
"pin-project",
"rand 0.8.5",
"tempfile",
"tokio",
"walkdir",
"zip",
]
[[package]]
@ -735,9 +843,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.3.7"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f"
checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
[[package]]
name = "lock_api"
@ -804,7 +912,7 @@ version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi 0.1.19",
"hermit-abi",
"libc",
]
@ -865,6 +973,29 @@ dependencies = [
"regex",
]
[[package]]
name = "password-hash"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
dependencies = [
"base64ct",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917"
dependencies = [
"digest",
"hmac",
"password-hash",
"sha2",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
@ -1024,6 +1155,18 @@ version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
[[package]]
name = "pkg-config"
version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.15"
@ -1252,16 +1395,16 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
dependencies = [
"bitflags",
"bitflags 1.3.2",
]
[[package]]
@ -1289,16 +1432,15 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.37.3"
version = "0.38.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b24138615de35e32031d041a09032ef3487a616d901ca4db224e7d557efae2"
checksum = "dc99bc2d4f1fed22595588a013687477aedf3cdcfb26558c559edb67b4d9b22e"
dependencies = [
"bitflags",
"bitflags 2.4.1",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@ -1359,6 +1501,17 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.6"
@ -1434,6 +1587,12 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "subtle"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.109"
@ -1458,15 +1617,15 @@ dependencies = [
[[package]]
name = "tempfile"
version = "3.5.0"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998"
checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall 0.3.5",
"redox_syscall 0.4.1",
"rustix",
"windows-sys 0.45.0",
"windows-sys 0.48.0",
]
[[package]]
@ -1540,6 +1699,24 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5"
dependencies = [
"deranged",
"powerfmt",
"serde",
"time-core",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "tokio"
version = "1.34.0"
@ -1722,9 +1899,9 @@ checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "walkdir"
version = "2.3.3"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
dependencies = [
"same-file",
"winapi-util",
@ -1857,37 +2034,13 @@ dependencies = [
"windows_x86_64_msvc 0.36.1",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.1",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7"
dependencies = [
"windows_aarch64_gnullvm 0.42.1",
"windows_aarch64_msvc 0.42.1",
"windows_i686_gnu 0.42.1",
"windows_i686_msvc 0.42.1",
"windows_x86_64_gnu 0.42.1",
"windows_x86_64_gnullvm 0.42.1",
"windows_x86_64_msvc 0.42.1",
"windows-targets",
]
[[package]]
@ -1896,21 +2049,15 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
@ -1923,12 +2070,6 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
@ -1941,12 +2082,6 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]]
name = "windows_i686_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
@ -1959,12 +2094,6 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]]
name = "windows_i686_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
@ -1977,24 +2106,12 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
@ -2007,14 +2124,57 @@ version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "zip"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261"
dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"flate2",
"hmac",
"pbkdf2",
"sha1",
"time",
"zstd",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "5.0.2+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.9+zstd.1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656"
dependencies = [
"cc",
"pkg-config",
]

View File

@ -15,3 +15,6 @@ rand = "0.8.5"
async-trait.workspace = true
md5 = "0.7.0"
anyhow.workspace = true
walkdir = "2.4.0"
zip = "0.6.6"
tempfile = "3.8.1"

View File

@ -0,0 +1,150 @@
use std::cmp::Ordering;
use std::fs::File;
use std::path::{Path, PathBuf};
use std::time::SystemTime;
use std::{fs, io};
use tempfile::tempdir;
use walkdir::WalkDir;
use zip::write::FileOptions;
use zip::ZipArchive;
use zip::ZipWriter;
pub fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> {
for entry in WalkDir::new(src).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
let relative_path = path.strip_prefix(src).unwrap();
let target_path = dst.join(relative_path);
if path.is_dir() {
fs::create_dir_all(&target_path)?;
} else {
fs::copy(path, target_path)?;
}
}
Ok(())
}
pub fn find_and_sort_folders_at<P>(path: &str, pat: P, order: Ordering) -> Vec<PathBuf>
where
P: Fn(&str) -> bool,
{
let mut folders = Vec::new();
for entry in WalkDir::new(path)
.min_depth(1)
.max_depth(1)
.into_iter()
.filter_map(|e| e.ok())
{
let entry_path = entry.path().to_path_buf();
if entry_path.is_dir()
&& entry_path
.file_name()
.unwrap_or_default()
.to_str()
.map(&pat)
.unwrap_or(false)
{
let metadata = fs::metadata(&entry_path).ok();
let modified_time = metadata
.and_then(|m| m.modified().ok())
.unwrap_or(SystemTime::UNIX_EPOCH);
folders.push((entry_path, modified_time));
}
}
// Sort folders based on the specified order
folders.sort_by(|a, b| match order {
Ordering::Less => a.1.cmp(&b.1),
Ordering::Greater => b.1.cmp(&a.1),
_ => a.1.cmp(&b.1), // Default case
});
// Extract just the PathBufs, discarding the modification times
folders.into_iter().map(|(path, _)| path).collect()
}
pub fn zip_folder(src_path: impl AsRef<Path>, dest_path: &Path) -> io::Result<()> {
if !src_path.as_ref().exists() {
return Err(io::ErrorKind::NotFound.into());
}
if src_path.as_ref() == dest_path {
return Err(io::ErrorKind::InvalidInput.into());
}
let file = File::create(dest_path)?;
let mut zip = ZipWriter::new(file);
let options = FileOptions::default().compression_method(zip::CompressionMethod::Stored);
for entry in WalkDir::new(&src_path) {
let entry = entry?;
let path = entry.path();
let name = match path.strip_prefix(&src_path) {
Ok(n) => n,
Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "Invalid path")),
};
if path.is_file() {
zip.start_file(
name
.to_str()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid file name"))?,
options,
)?;
let mut f = File::open(path)?;
io::copy(&mut f, &mut zip)?;
} else if !name.as_os_str().is_empty() {
zip.add_directory(
name
.to_str()
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Invalid directory name"))?,
options,
)?;
}
}
zip.finish()?;
Ok(())
}
pub fn unzip_and_replace(zip_path: impl AsRef<Path>, target_folder: &Path) -> io::Result<()> {
// Create a temporary directory for unzipping
let temp_dir = tempdir()?;
// Unzip the file
let file = File::open(zip_path.as_ref())?;
let mut archive = ZipArchive::new(file)?;
for i in 0..archive.len() {
let mut file = archive.by_index(i)?;
let outpath = temp_dir.path().join(file.mangled_name());
if file.name().ends_with('/') {
fs::create_dir_all(&outpath)?;
} else {
if let Some(p) = outpath.parent() {
if !p.exists() {
fs::create_dir_all(p)?;
}
}
let mut outfile = File::create(&outpath)?;
io::copy(&mut file, &mut outfile)?;
}
}
// Replace the contents of the target folder
if target_folder.exists() {
fs::remove_dir_all(target_folder)?;
}
fs::create_dir_all(target_folder)?;
for entry in fs::read_dir(temp_dir.path())? {
let entry = entry?;
fs::rename(entry.path(), target_folder.join(entry.file_name()))?;
}
Ok(())
}

View File

@ -1,6 +1,7 @@
pub use async_trait;
pub mod box_any;
pub mod file_util;
pub mod future;
pub mod ref_map;
pub mod util;

View File

@ -1,10 +1,11 @@
use super::cursor::*;
use std::ops::{Deref, DerefMut};
use crate::core::delta::operation::{DeltaOperation, OperationAttributes};
use crate::core::delta::{DeltaOperations, NEW_LINE};
use crate::core::interval::Interval;
use crate::core::AttributeHashMap;
use std::ops::{Deref, DerefMut};
use super::cursor::*;
pub(crate) const MAX_IV_LEN: usize = i32::MAX as usize;
@ -93,7 +94,7 @@ where
pub fn seek<M: Metric>(&mut self, index: usize) {
match M::seek(&mut self.cursor, index) {
Ok(_) => {},
Err(e) => log::error!("Seek fail: {:?}", e),
Err(e) => tracing::error!("Seek fail: {:?}", e),
}
}

View File

@ -1,7 +1,3 @@
use crate::core::interval::Interval;
use crate::core::ot_str::OTString;
use crate::errors::OTError;
use serde::{Deserialize, Serialize, __private::Formatter};
use std::fmt::Display;
use std::{
cmp::min,
@ -10,6 +6,12 @@ use std::{
ops::{Deref, DerefMut},
};
use serde::{Deserialize, Serialize, __private::Formatter};
use crate::core::interval::Interval;
use crate::core::ot_str::OTString;
use crate::errors::OTError;
pub trait OperationTransform {
/// Merges the operation with `other` into one operation while preserving
/// the changes of both.
@ -156,7 +158,7 @@ where
pub fn set_attributes(&mut self, attributes: T) {
match self {
DeltaOperation::Delete(_) => log::error!("Delete should not contains attributes"),
DeltaOperation::Delete(_) => tracing::error!("Delete should not contains attributes"),
DeltaOperation::Retain(retain) => retain.attributes = attributes,
DeltaOperation::Insert(insert) => insert.attributes = attributes,
}

View File

@ -1,3 +1,14 @@
use std::{
cmp::{min, Ordering},
fmt,
iter::FromIterator,
str,
str::FromStr,
};
use bytes::Bytes;
use serde::de::DeserializeOwned;
use crate::core::delta::operation::{
DeltaOperation, EmptyAttributes, OperationAttributes, OperationTransform,
};
@ -6,15 +17,6 @@ use crate::core::interval::Interval;
use crate::core::ot_str::OTString;
use crate::core::DeltaOperationBuilder;
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
use bytes::Bytes;
use serde::de::DeserializeOwned;
use std::{
cmp::{min, Ordering},
fmt,
iter::FromIterator,
str,
str::FromStr,
};
pub type DeltaBuilder = DeltaOperationBuilder<EmptyAttributes>;
@ -564,7 +566,7 @@ fn invert_other<T: OperationAttributes>(
base.retain(other_op.len(), inverted_attrs);
},
DeltaOperation::Insert(_) => {
log::error!("Impossible to here. Insert operation should be treated as delete")
tracing::error!("Impossible to here. Insert operation should be treated as delete")
},
});
}