Compare commits

...
Author SHA1 Message Date
Lucas.Xu 88e008e34e chore: bump version 0.6.7.2 2024-08-20 17:52:20 +08:00
Lucas.Xu 8e050604f2 fix: ignore case sensitive of image name when dragging image to document (#6017) 2024-08-20 16:56:07 +08:00
Lucas.Xu bbec9ffd0f fix: unable to insert todo list via slash menu (#5980)
* fix: unable to insert todo list via slash menu

* fix: unable to insert divider via slash menu

* chore: update editor version

* chore: update translations

* chore: decrease sentry sample rate to 0.1

* fix: integration test
2024-08-20 16:19:50 +08:00
Nathan.fooo e9adaae333 chore: fix file upload test (#6016)
* chore: fix file upload test
2024-08-20 15:20:03 +08:00
Lucas.Xu 495601b683 chore: bump version 0.6.7.1 (#5964) 2024-08-14 22:08:02 +08:00
nathan f23787fec7 chore: fix disable local ai 2024-08-14 21:01:49 +08:00
Nathan.fooo a66fba2753 chore: remove workspac id in user profile (#5962)
* chore: remove workspac id in user profile

* chore: fix test

* chore: clippy

* chore: clippy

* chore: fix cloud test

* chore: fix checklist test
2024-08-14 19:48:36 +08:00
Nathan.fooo 2c5407da02 fix: refresh local ai state when opening workspace (#5961)
* chore: fix local ai state when open other workspace

* chore: fix duplicate message
2024-08-14 19:47:56 +08:00
36 changed files with 416 additions and 253 deletions

No files matched your search

+4
View File
@@ -1,4 +1,8 @@
# Release Notes
## Version 0.6.7.1 - 14/08/2024
### Bug Fixes
- Fixed the issue where workspace id was not compatible with settings page.
## Version 0.6.7 - 13/08/2024
### New Features
- Redesigned the icon picker design on Desktop.
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.7"
APPFLOWY_VERSION = "0.6.7.2"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -47,31 +47,28 @@ void main() {
await tester.openSettingsPage(SettingsPage.account);
await tester.enterUserName(name);
await tester.tapEscButton();
// wait 2 seconds for the sync to finish
await tester.pumpAndSettle(const Duration(seconds: 6));
});
await tester.logout();
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(profileSetting.name, name);
await tester.pumpAndSettle(const Duration(seconds: 2));
});
});
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
);
await tester.tapGoogleLoginInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
await tester.pumpAndSettle();
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
// Verify name
final profileSetting =
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
expect(profileSetting.name, name);
});
}
@@ -461,22 +461,22 @@ void main() {
tester.assertChecklistEditorVisible(visible: true);
// create a new task with enter
await tester.createNewChecklistTask(name: "task 0", enter: true);
await tester.createNewChecklistTask(name: "task 1", enter: true);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 0",
name: "task 1",
isChecked: false,
);
// update the task's name
await tester.renameChecklistTask(index: 0, name: "task 1");
await tester.renameChecklistTask(index: 0, name: "task 11");
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
name: "task 1",
name: "task 11",
isChecked: false,
);
@@ -176,6 +176,7 @@ Future<void> createInlineDatabase(
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
layout.slashMenuName,
offset: 100,
);
await tester.pumpAndSettle();
@@ -80,7 +80,7 @@ extension AppFlowySettings on WidgetTester {
of: find.byType(UserProfileSetting),
matching: find.byFlowySvg(FlowySvgs.edit_s),
);
await tap(editUsernameFinder);
await tap(editUsernameFinder, warnIfMissed: false);
await pumpAndSettle();
final userNameFinder = find.descendant(
@@ -195,13 +195,15 @@ class _DocumentPageState extends State<DocumentPage>
final isLocalMode = context.read<DocumentBloc>().isLocalMode;
final List<XFile> imageFiles = [];
final List<XFile> otherfiles = [];
final List<XFile> otherFiles = [];
for (final file in details.files) {
final fileName = file.name.toLowerCase();
if (file.mimeType?.startsWith('image/') ??
false || imgExtensionRegex.hasMatch(file.name)) {
false || imgExtensionRegex.hasMatch(fileName)) {
imageFiles.add(file);
} else {
otherfiles.add(file);
otherFiles.add(file);
}
}
@@ -213,7 +215,7 @@ class _DocumentPageState extends State<DocumentPage>
);
await editorState!.dropFiles(
data.dropTarget!,
otherfiles,
otherFiles,
widget.view.id,
isLocalMode,
);
@@ -410,6 +410,7 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
}
List<SelectionMenuItem> _customSlashMenuItems() {
return [
aiWriterSlashMenuItem,
textSlashMenuItem,
@@ -419,7 +420,10 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
imageSlashMenuItem,
bulletedListSlashMenuItem,
numberedListSlashMenuItem,
todoListSlashMenuItem,
dividerSlashMenuItem,
quoteSlashMenuItem,
tableSlashMenuItem,
referencedDocSlashMenuItem,
gridSlashMenuItem(documentBloc),
referencedGridSlashMenuItem,
@@ -34,7 +34,7 @@ extension PasteFromImage on EditorState {
final imageFiles = files.where(
(file) =>
file.mimeType?.startsWith('image/') ??
false || imgExtensionRegex.hasMatch(file.name),
false || imgExtensionRegex.hasMatch(file.name.toLowerCase()),
);
for (final file in imageFiles) {
@@ -125,6 +125,21 @@ final numberedListSlashMenuItem = SelectionMenuItem(
},
);
// todo list menu item
final todoListSlashMenuItem = SelectionMenuItem(
getName: () => LocaleKeys.document_slashMenu_name_todoList.tr(),
nameBuilder: _slashMenuItemNameBuilder,
icon: (editorState, isSelected, style) => SelectableSvgWidget(
data: FlowySvgs.slash_menu_icon_checkbox_s,
isSelected: isSelected,
style: style,
),
keywords: ['checkbox', 'todo', 'list', 'to-do', 'task'],
handler: (editorState, _, __) {
insertCheckboxAfterSelection(editorState);
},
);
// quote menu item
final quoteSlashMenuItem = SelectionMenuItem(
getName: () => LocaleKeys.document_slashMenu_name_quote.tr(),
@@ -134,12 +149,42 @@ final quoteSlashMenuItem = SelectionMenuItem(
isSelected: isSelected,
style: style,
),
keywords: ['quote', 'refer'],
keywords: ['quote', 'refer', 'blockquote', 'citation'],
handler: (editorState, _, __) {
insertQuoteAfterSelection(editorState);
},
);
// divider menu item
final dividerSlashMenuItem = SelectionMenuItem(
getName: () => LocaleKeys.document_slashMenu_name_divider.tr(),
nameBuilder: _slashMenuItemNameBuilder,
icon: (editorState, isSelected, style) => SelectableSvgWidget(
data: FlowySvgs.slash_menu_icon_divider_s,
isSelected: isSelected,
style: style,
),
keywords: ['divider', 'separator', 'line', 'break', 'horizontal line'],
handler: (editorState, _, __) {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final path = selection.end.path;
final node = editorState.getNodeAtPath(path);
final delta = node?.delta;
if (node == null || delta == null) {
return;
}
final insertedPath = delta.isEmpty ? path : path.next;
final transaction = editorState.transaction
..insertNode(insertedPath, dividerNode())
..insertNode(insertedPath, paragraphNode())
..afterSelection = Selection.collapsed(Position(path: insertedPath.next));
editorState.apply(transaction);
},
);
// grid & board & calendar menu item
SelectionMenuItem gridSlashMenuItem(DocumentBloc documentBloc) {
return SelectionMenuItem(
@@ -347,7 +392,7 @@ SelectionMenuItem toggleListSlashMenuItem = SelectionMenuItem.node(
isSelected: isSelected,
style: style,
),
keywords: ['collapsed list', 'toggle list', 'list'],
keywords: ['collapsed list', 'toggle list', 'list', 'dropdown'],
nodeBuilder: (editorState, _) => toggleListBlockNode(),
replace: (_, node) => node.delta?.isEmpty ?? false,
);
@@ -361,7 +406,7 @@ SelectionMenuItem emojiSlashMenuItem = SelectionMenuItem(
isSelected: isSelected,
style: style,
),
keywords: ['emoji'],
keywords: ['emoji', 'reaction', 'emoticon'],
handler: (editorState, menuService, context) {
final container = Overlay.of(context);
menuService.dismiss();
@@ -391,6 +436,56 @@ SelectionMenuItem aiWriterSlashMenuItem = SelectionMenuItem.node(
replace: (_, node) => false,
);
// table menu item
SelectionMenuItem tableSlashMenuItem = SelectionMenuItem(
getName: () => LocaleKeys.document_slashMenu_name_table.tr(),
nameBuilder: _slashMenuItemNameBuilder,
icon: (editorState, isSelected, style) => SelectableSvgWidget(
data: FlowySvgs.slash_menu_icon_simple_table_s,
isSelected: isSelected,
style: style,
),
keywords: ['table', 'rows', 'columns', 'data'],
handler: (editorState, _, __) async {
final selection = editorState.selection;
if (selection == null || !selection.isCollapsed) {
return;
}
final currentNode = editorState.getNodeAtPath(selection.end.path);
if (currentNode == null) {
return;
}
final tableNode = TableNode.fromList([
['', ''],
['', ''],
]);
final transaction = editorState.transaction;
final delta = currentNode.delta;
if (delta != null && delta.isEmpty) {
transaction
..insertNode(selection.end.path, tableNode.node)
..deleteNode(currentNode);
transaction.afterSelection = Selection.collapsed(
Position(
path: selection.end.path + [0, 0],
),
);
} else {
transaction.insertNode(selection.end.path.next, tableNode.node);
transaction.afterSelection = Selection.collapsed(
Position(
path: selection.end.path.next + [0, 0],
),
);
}
await editorState.apply(transaction);
},
);
// date or reminder menu item
SelectionMenuItem dateOrReminderSlashMenuItem = SelectionMenuItem(
getName: () => LocaleKeys.document_slashMenu_name_dateOrReminder.tr(),
@@ -400,7 +495,7 @@ SelectionMenuItem dateOrReminderSlashMenuItem = SelectionMenuItem(
isSelected: isSelected,
style: style,
),
keywords: ['insert date', 'date', 'time', 'reminder'],
keywords: ['insert date', 'date', 'time', 'reminder', 'schedule'],
handler: (editorState, menuService, context) =>
insertDateReference(editorState),
);
@@ -439,7 +534,7 @@ SelectionMenuItem fileSlashMenuItem = SelectionMenuItem(
isSelected: isSelected,
style: style,
),
keywords: ['file upload', 'pdf', 'zip', 'archive', 'upload'],
keywords: ['file upload', 'pdf', 'zip', 'archive', 'upload', 'attachment'],
handler: (editorState, _, __) async => editorState.insertEmptyFileBlock(),
);
@@ -15,8 +15,11 @@ part 'local_ai_on_boarding_bloc.freezed.dart';
class LocalAIOnBoardingBloc
extends Bloc<LocalAIOnBoardingEvent, LocalAIOnBoardingState> {
LocalAIOnBoardingBloc(this.userProfile)
: super(const LocalAIOnBoardingState()) {
LocalAIOnBoardingBloc(
this.userProfile,
this.member,
this.workspaceId,
) : super(const LocalAIOnBoardingState()) {
_userService = UserBackendService(userId: userProfile.id);
_successListenable = getIt<SubscriptionSuccessListenable>();
_successListenable.addListener(_onPaymentSuccessful);
@@ -36,6 +39,8 @@ class LocalAIOnBoardingBloc
}
final UserProfilePB userProfile;
final WorkspaceMemberPB member;
final String workspaceId;
late final IUserBackendService _userService;
late final SubscriptionSuccessListenable _successListenable;
@@ -48,7 +53,7 @@ class LocalAIOnBoardingBloc
addSubscription: (plan) async {
emit(state.copyWith(isLoading: true));
final result = await _userService.createSubscription(
userProfile.workspaceId,
workspaceId,
plan,
);
@@ -72,7 +77,7 @@ class LocalAIOnBoardingBloc
);
},
(err) {
Log.error("Failed to get subscription plans: $err");
Log.warn("Failed to get subscription plans: $err");
},
);
},
@@ -86,7 +91,7 @@ class LocalAIOnBoardingBloc
}
void _loadSubscriptionPlans() {
final payload = UserWorkspaceIdPB()..workspaceId = userProfile.workspaceId;
final payload = UserWorkspaceIdPB()..workspaceId = workspaceId;
UserEventGetWorkspaceSubscriptionInfo(payload).send().then((result) {
if (!isClosed) {
add(LocalAIOnBoardingEvent.didGetSubscriptionPlans(result));
@@ -11,8 +11,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'settings_ai_bloc.freezed.dart';
class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
SettingsAIBloc(this.userProfile, WorkspaceMemberPB? member)
: _userListener = UserListener(userProfile: userProfile),
SettingsAIBloc(
this.userProfile,
this.workspaceId,
WorkspaceMemberPB? member,
) : _userListener = UserListener(userProfile: userProfile),
_userService = UserBackendService(userId: userProfile.id),
super(SettingsAIState(userProfile: userProfile, member: member)) {
_dispatch();
@@ -36,6 +39,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
final UserListener _userListener;
final UserProfilePB userProfile;
final UserBackendService _userService;
final String workspaceId;
@override
Future<void> close() async {
@@ -92,7 +96,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
AIModelPB? model,
}) {
final payload = UpdateUserWorkspaceSettingPB(
workspaceId: userProfile.workspaceId,
workspaceId: workspaceId,
);
if (disableSearchIndexing != null) {
payload.disableSearchIndexing = disableSearchIndexing;
@@ -112,7 +116,7 @@ class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
);
void _loadUserWorkspaceSetting() {
final payload = UserWorkspaceIdPB(workspaceId: userProfile.workspaceId);
final payload = UserWorkspaceIdPB(workspaceId: workspaceId);
UserEventGetWorkspaceSetting(payload).send().then((result) {
result.fold((settings) {
if (!isClosed) {
@@ -133,7 +137,8 @@ class SettingsAIEvent with _$SettingsAIEvent {
) = _DidLoadWorkspaceSetting;
const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch;
const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) = _RefreshMember;
const factory SettingsAIEvent.refreshMember(WorkspaceMemberPB member) =
_RefreshMember;
const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel;
@@ -40,15 +40,17 @@ class SettingsAIView extends StatelessWidget {
super.key,
required this.userProfile,
required this.member,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB? member;
final String workspaceId;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) => SettingsAIBloc(userProfile, member)
create: (_) => SettingsAIBloc(userProfile, workspaceId, member)
..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
@@ -63,6 +65,7 @@ class SettingsAIView extends StatelessWidget {
_LocalAIOnBoarding(
userProfile: userProfile,
member: state.member!,
workspaceId: workspaceId,
),
);
}
@@ -127,9 +130,11 @@ class _LocalAIOnBoarding extends StatelessWidget {
const _LocalAIOnBoarding({
required this.userProfile,
required this.member,
required this.workspaceId,
});
final UserProfilePB userProfile;
final WorkspaceMemberPB member;
final String workspaceId;
@override
Widget build(BuildContext context) {
@@ -137,8 +142,9 @@ class _LocalAIOnBoarding extends StatelessWidget {
return BillingGateGuard(
builder: (context) {
return BlocProvider(
create: (context) => LocalAIOnBoardingBloc(userProfile)
..add(const LocalAIOnBoardingEvent.started()),
create: (context) =>
LocalAIOnBoardingBloc(userProfile, member, workspaceId)
..add(const LocalAIOnBoardingEvent.started()),
child: BlocBuilder<LocalAIOnBoardingBloc, LocalAIOnBoardingState>(
builder: (context, state) {
// Show the local AI settings if the user has purchased the AI Local plan
@@ -120,7 +120,11 @@ class SettingsDialog extends StatelessWidget {
return const SettingsShortcutsView();
case SettingsPage.ai:
if (user.authenticator == AuthenticatorPB.AppFlowyCloud) {
return SettingsAIView(userProfile: user, member: member);
return SettingsAIView(
userProfile: user,
member: member,
workspaceId: workspaceId,
);
} else {
return const AIFeatureOnlySupportedWhenUsingAppFlowyCloud();
}
+6 -30
View File
@@ -53,11 +53,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: "9d3e854"
resolved-ref: "9d3e854f11fd9d732535ce5f5b1c8f41517479a1"
ref: "8e17d14"
resolved-ref: "8e17d1447eea0b57ff92e31dbe88796ce759fb37"
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "3.1.0"
version: "3.2.0"
appflowy_editor_plugins:
dependency: "direct main"
description:
@@ -839,7 +839,7 @@ packages:
source: hosted
version: "0.6.5"
flutter_svg:
dependency: "direct main"
dependency: transitive
description:
name: flutter_svg
sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c
@@ -1122,14 +1122,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.0"
intl_utils:
dependency: transitive
description:
name: intl_utils
sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4
url: "https://pub.dev"
source: hosted
version: "2.8.7"
io:
dependency: transitive
description:
@@ -1155,7 +1147,7 @@ packages:
source: hosted
version: "0.7.0"
isolates:
dependency: "direct main"
dependency: transitive
description:
name: isolates
sha256: ce89e4141b27b877326d3715be2dceac7a7ba89f3229785816d2d318a75ddf28
@@ -1275,7 +1267,7 @@ packages:
source: hosted
version: "0.1.5"
logger:
dependency: "direct main"
dependency: transitive
description:
name: logger
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
@@ -1514,14 +1506,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.11.1"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
percent_indicator:
dependency: "direct main"
description:
@@ -1642,14 +1626,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
printing:
dependency: transitive
description:
name: printing
sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3
url: "https://pub.dev"
source: hosted
version: "5.13.1"
process:
dependency: transitive
description:
+2 -5
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.7
version: 0.6.7+2
environment:
flutter: ">=3.22.0"
@@ -81,7 +81,6 @@ dependencies:
linked_scroll_controller: ^0.2.0
hotkey_manager: ^0.1.7
fixnum: ^1.1.0
flutter_svg: ^2.0.7
protobuf: ^3.1.0
charcode: ^1.3.1
collection: ^1.17.1
@@ -140,10 +139,8 @@ dependencies:
auto_size_text_field: ^2.2.3
reorderable_tabbar: ^1.0.6
shimmer: ^3.0.0
isolates: ^3.0.3+8
markdown_widget: ^2.3.2+6
markdown:
logger: ^2.4.0
# Desktop Drop uses Cross File (XFile) data type
desktop_drop: ^0.4.4
@@ -199,7 +196,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "9d3e854"
ref: "8e17d14"
appflowy_editor_plugins:
git:
+3 -2
View File
@@ -1327,6 +1327,7 @@
"addOption": "Add option",
"editProperty": "Edit property",
"newProperty": "New property",
"openRowDocument": "Open as a page",
"deleteFieldPromptMessage": "Are you sure? This property will be deleted",
"clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied",
"newColumn": "New Column",
@@ -1470,7 +1471,7 @@
"image": "Image",
"bulletedList": "Bulleted List",
"numberedList": "Numbered List",
"checkbox": "Checkbox",
"todoList": "To-do List",
"doc": "Doc",
"linkedDoc": "Link to page",
"grid": "Grid",
@@ -2411,4 +2412,4 @@
"commentAddedSuccessfully": "Comment added successfully.",
"commentAddedSuccessTip": "You've just added or replied to a comment. Would you like to jump to the top to see the latest comments?"
}
}
}
@@ -2,6 +2,7 @@ use crate::document::generate_random_bytes;
use event_integration_test::user_event::user_localhost_af_cloud;
use event_integration_test::EventIntegrationTest;
use flowy_storage_pub::storage::FileUploadState;
use lib_infra::util::md5;
use std::env::temp_dir;
use std::sync::Arc;
use std::time::Duration;
@@ -9,19 +10,21 @@ use tokio::fs;
use tokio::fs::File;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use tokio::time::timeout;
#[tokio::test]
async fn af_cloud_upload_big_file_test() {
user_localhost_af_cloud().await;
let mut test = EventIntegrationTest::new().await;
test.af_cloud_sign_up().await;
tokio::time::sleep(Duration::from_secs(6)).await;
let parent_dir = "temp_test";
let workspace_id = test.get_current_workspace().await.id;
let (file_path, upload_data) = generate_file_with_bytes_len(15 * 1024 * 1024).await;
let (created_upload, rx) = test
.storage_manager
.storage_service
.create_upload(&workspace_id, "temp_test", &file_path, false)
.create_upload(&workspace_id, parent_dir, &file_path, false)
.await
.unwrap();
@@ -42,15 +45,22 @@ async fn af_cloud_upload_big_file_test() {
// Restart the test. It will load unfinished uploads
let test = EventIntegrationTest::new_with_config(config).await;
let mut rx = test
if let Some(mut rx) = test
.storage_manager
.subscribe_file_state(&created_upload.file_id)
.subscribe_file_state(parent_dir, &created_upload.file_id)
.await
.unwrap();
while let Some(state) = rx.recv().await {
if let FileUploadState::Finished { .. } = state {
break;
.unwrap()
{
let timeout_duration = Duration::from_secs(180);
while let Some(state) = match timeout(timeout_duration, rx.recv()).await {
Ok(result) => result,
Err(_) => {
panic!("Timed out waiting for file upload completion");
},
} {
if let FileUploadState::Finished { .. } = state {
break;
}
}
}
@@ -62,8 +72,7 @@ async fn af_cloud_upload_big_file_test() {
.file_storage()
.unwrap();
let file = file_service.get_object(created_upload.url).await.unwrap();
assert_eq!(file.raw.to_vec(), upload_data);
assert_eq!(md5(file.raw), md5(upload_data));
let _ = fs::remove_file(file_path).await;
}
@@ -38,19 +38,18 @@ async fn af_cloud_add_workspace_member_test() {
user_localhost_af_cloud().await;
let test_1 = EventIntegrationTest::new().await;
let user_1 = test_1.af_cloud_sign_up().await;
let workspace_id_1 = test_1.get_current_workspace().await.id;
let test_2 = EventIntegrationTest::new().await;
let user_2 = test_2.af_cloud_sign_up().await;
let members = test_1.get_workspace_members(&user_1.workspace_id).await;
let members = test_1.get_workspace_members(&workspace_id_1).await;
assert_eq!(members.len(), 1);
assert_eq!(members[0].email, user_1.email);
test_1
.add_workspace_member(&user_1.workspace_id, &test_2)
.await;
test_1.add_workspace_member(&workspace_id_1, &test_2).await;
let members = test_1.get_workspace_members(&user_1.workspace_id).await;
let members = test_1.get_workspace_members(&workspace_id_1).await;
assert_eq!(members.len(), 2);
assert_eq!(members[0].email, user_1.email);
assert_eq!(members[1].email, user_2.email);
@@ -61,19 +60,18 @@ async fn af_cloud_delete_workspace_member_test() {
user_localhost_af_cloud().await;
let test_1 = EventIntegrationTest::new().await;
let user_1 = test_1.af_cloud_sign_up().await;
let workspace_id_1 = test_1.get_current_workspace().await.id;
let test_2 = EventIntegrationTest::new().await;
let user_2 = test_2.af_cloud_sign_up().await;
test_1
.add_workspace_member(&user_1.workspace_id, &test_2)
.await;
test_1.add_workspace_member(&workspace_id_1, &test_2).await;
test_1
.delete_workspace_member(&user_1.workspace_id, &user_2.email)
.delete_workspace_member(&workspace_id_1, &user_2.email)
.await;
let members = test_1.get_workspace_members(&user_1.workspace_id).await;
let members = test_1.get_workspace_members(&workspace_id_1).await;
assert_eq!(members.len(), 1);
assert_eq!(members[0].email, user_1.email);
}
@@ -82,21 +80,20 @@ async fn af_cloud_delete_workspace_member_test() {
async fn af_cloud_leave_workspace_test() {
user_localhost_af_cloud().await;
let test_1 = EventIntegrationTest::new().await;
let user_1 = test_1.af_cloud_sign_up().await;
test_1.af_cloud_sign_up().await;
let workspace_id_1 = test_1.get_current_workspace().await.id;
let test_2 = EventIntegrationTest::new().await;
let user_2 = test_2.af_cloud_sign_up().await;
test_1
.add_workspace_member(&user_1.workspace_id, &test_2)
.await;
test_1.add_workspace_member(&workspace_id_1, &test_2).await;
// test_2 should have 2 workspace
let workspaces = get_synced_workspaces(&test_2, user_2.id).await;
assert_eq!(workspaces.len(), 2);
// user_2 leaves the workspace
test_2.leave_workspace(&user_1.workspace_id).await;
test_2.leave_workspace(&workspace_id_1).await;
// user_2 should have 1 workspace
let workspaces = get_synced_workspaces(&test_2, user_2.id).await;
@@ -162,9 +162,6 @@ async fn af_cloud_different_open_same_workspace_test() {
let owner_profile = client_1.af_cloud_sign_up().await;
let shared_workspace_id = client_1.get_current_workspace().await.id.clone();
// Verify that the workspace ID from the profile matches the current session's workspace ID.
assert_eq!(shared_workspace_id, owner_profile.workspace_id);
// Define the number of additional clients
let num_clients = 5;
let mut clients = Vec::new();
@@ -182,7 +179,7 @@ async fn af_cloud_different_open_same_workspace_test() {
}
client_1
.add_workspace_member(&owner_profile.workspace_id, &client)
.add_workspace_member(&shared_workspace_id, &client)
.await;
clients.push((client, client_profile));
}
@@ -26,7 +26,6 @@ async fn anon_user_profile_get() {
assert_eq!(user_profile.id, user.id);
assert_eq!(user_profile.openai_key, user.openai_key);
assert_eq!(user_profile.stability_ai_key, user.stability_ai_key);
assert_eq!(user_profile.workspace_id, user.workspace_id);
assert_eq!(user_profile.authenticator, AuthenticatorPB::Local);
}
@@ -66,6 +66,11 @@ impl AIManager {
}
}
pub async fn initialize(&self, _workspace_id: &str) -> Result<(), FlowyError> {
self.local_ai_controller.refresh().await?;
Ok(())
}
pub async fn open_chat(&self, chat_id: &str) -> Result<(), FlowyError> {
trace!("open chat: {}", chat_id);
self.chats.entry(chat_id.to_string()).or_insert_with(|| {
+25 -29
View File
@@ -140,11 +140,7 @@ impl Chat {
let _ = question_sink.send(StreamMessage::Done.to_string()).await;
// Save message to disk
save_chat_message(
self.user_service.sqlite_connection(uid)?,
&self.chat_id,
vec![question.clone()],
)?;
save_and_notify_message(uid, &self.chat_id, &self.user_service, question.clone())?;
let stop_stream = self.stop_stream.clone();
let chat_id = self.chat_id.clone();
@@ -222,7 +218,7 @@ impl Chat {
let answer = cloud_service
.create_answer(&workspace_id, &chat_id, &content, question_id, metadata)
.await?;
Self::save_answer(uid, &chat_id, &user_service, answer)?;
save_and_notify_message(uid, &chat_id, &user_service, answer)?;
Ok::<(), FlowyError>(())
});
@@ -230,26 +226,6 @@ impl Chat {
Ok(question_pb)
}
fn save_answer(
uid: i64,
chat_id: &str,
user_service: &Arc<dyn AIUserService>,
answer: ChatMessage,
) -> Result<(), FlowyError> {
trace!("[Chat] save answer: answer={:?}", answer);
save_chat_message(
user_service.sqlite_connection(uid)?,
chat_id,
vec![answer.clone()],
)?;
let pb = ChatMessagePB::from(answer);
make_notification(chat_id, ChatNotification::DidReceiveChatMessage)
.payload(pb)
.send();
Ok(())
}
/// Load chat messages for a given `chat_id`.
///
/// 1. When opening a chat:
@@ -375,7 +351,7 @@ impl Chat {
{
Ok(resp) => {
// Save chat messages to local disk
if let Err(err) = save_chat_message(
if let Err(err) = save_chat_message_disk(
user_service.sqlite_connection(uid)?,
&chat_id,
resp.messages.clone(),
@@ -453,7 +429,7 @@ impl Chat {
.get_answer(&workspace_id, &self.chat_id, question_message_id)
.await?;
Self::save_answer(self.uid, &self.chat_id, &self.user_service, answer.clone())?;
save_and_notify_message(self.uid, &self.chat_id, &self.user_service, answer.clone())?;
let pb = ChatMessagePB::from(answer);
Ok(pb)
}
@@ -527,7 +503,7 @@ impl Chat {
}
}
fn save_chat_message(
fn save_chat_message_disk(
conn: DBConnection,
chat_id: &str,
messages: Vec<ChatMessage>,
@@ -581,3 +557,23 @@ impl StringBuffer {
std::mem::take(&mut self.content)
}
}
pub(crate) fn save_and_notify_message(
uid: i64,
chat_id: &str,
user_service: &Arc<dyn AIUserService>,
message: ChatMessage,
) -> Result<(), FlowyError> {
trace!("[Chat] save answer: answer={:?}", message);
save_chat_message_disk(
user_service.sqlite_connection(uid)?,
chat_id,
vec![message.clone()],
)?;
let pb = ChatMessagePB::from(message);
make_notification(chat_id, ChatNotification::DidReceiveChatMessage)
.payload(pb)
.send();
Ok(())
}
@@ -171,7 +171,7 @@ pub(crate) async fn refresh_local_ai_info_handler(
let ai_manager = upgrade_ai_manager(ai_manager)?;
let (tx, rx) = oneshot::channel::<Result<LLMModelInfo, FlowyError>>();
tokio::spawn(async move {
let model_info = ai_manager.local_ai_controller.refresh().await;
let model_info = ai_manager.local_ai_controller.refresh_model_info().await;
if model_info.is_err() {
if let Some(llm_model) = ai_manager.local_ai_controller.get_current_model() {
let model_info = LLMModelInfo {
@@ -49,6 +49,7 @@ pub struct LocalAIController {
local_ai_resource: Arc<LocalAIResourceController>,
current_chat_id: Mutex<Option<String>>,
store_preferences: Arc<KVStorePreferences>,
user_service: Arc<dyn AIUserService>,
}
impl Deref for LocalAIController {
@@ -74,7 +75,11 @@ impl LocalAIController {
};
let (tx, mut rx) = tokio::sync::mpsc::channel(1);
let llm_res = Arc::new(LocalAIResourceController::new(user_service, res_impl, tx));
let llm_res = Arc::new(LocalAIResourceController::new(
user_service.clone(),
res_impl,
tx,
));
let current_chat_id = Mutex::new(None);
let mut running_state_rx = local_ai.subscribe_running_state();
@@ -101,6 +106,7 @@ impl LocalAIController {
local_ai_resource: llm_res,
current_chat_id,
store_preferences,
user_service,
};
let rag_enabled = this.is_rag_enabled();
@@ -142,7 +148,13 @@ impl LocalAIController {
this
}
pub async fn refresh(&self) -> FlowyResult<LLMModelInfo> {
pub async fn refresh(&self) -> FlowyResult<()> {
let is_enabled = self.is_enabled();
self.enable_chat_plugin(is_enabled).await?;
Ok(())
}
pub async fn refresh_model_info(&self) -> FlowyResult<LLMModelInfo> {
self.local_ai_resource.refresh_llm_resource().await
}
@@ -158,10 +170,16 @@ impl LocalAIController {
/// Indicate whether the local AI is enabled.
pub fn is_enabled(&self) -> bool {
self
.store_preferences
.get_bool(APPFLOWY_LOCAL_AI_ENABLED)
.unwrap_or(true)
if let Ok(key) = self.local_ai_enabled_key() {
self.store_preferences.get_bool(&key).unwrap_or(true)
} else {
false
}
}
fn local_ai_enabled_key(&self) -> FlowyResult<String> {
let workspace_id = self.user_service.workspace_id()?;
Ok(format!("{}:{}", APPFLOWY_LOCAL_AI_ENABLED, workspace_id))
}
/// Indicate whether the local AI chat is enabled. In the future, we can support multiple
@@ -297,13 +315,9 @@ impl LocalAIController {
}
pub async fn toggle_local_ai(&self) -> FlowyResult<bool> {
let enabled = !self
.store_preferences
.get_bool(APPFLOWY_LOCAL_AI_ENABLED)
.unwrap_or(true);
self
.store_preferences
.set_bool(APPFLOWY_LOCAL_AI_ENABLED, enabled)?;
let key = self.local_ai_enabled_key()?;
let enabled = !self.store_preferences.get_bool(&key).unwrap_or(true);
self.store_preferences.set_bool(&key, enabled)?;
// when enable local ai. we need to check if chat is enabled, if enabled, we need to init chat plugin
// otherwise, we need to destroy the plugin
@@ -312,9 +326,12 @@ impl LocalAIController {
.store_preferences
.get_bool(APPFLOWY_LOCAL_AI_CHAT_ENABLED)
.unwrap_or(true);
self.enable_chat_plugin(chat_enabled).await?;
if self.local_ai_resource.is_resource_ready() {
self.enable_chat_plugin(chat_enabled).await?;
}
} else {
self.enable_chat_plugin(false).await?;
let _ = self.enable_chat_plugin(false).await;
}
Ok(enabled)
}
@@ -196,7 +196,7 @@ impl StorageService for DocumentTestFileStorageService {
todo!()
}
async fn start_upload(&self, _chunks: &ChunkedBytes, _record: &BoxAny) -> Result<(), FlowyError> {
async fn start_upload(&self, _chunks: ChunkedBytes, _record: &BoxAny) -> Result<(), FlowyError> {
todo!()
}
@@ -209,7 +209,11 @@ impl StorageService for DocumentTestFileStorageService {
todo!()
}
async fn subscribe_file_progress(&self, _url: &str) -> Result<FileProgressReceiver, FlowyError> {
async fn subscribe_file_progress(
&self,
_parent_idr: &str,
_url: &str,
) -> Result<Option<FileProgressReceiver>, FlowyError> {
todo!()
}
}
@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE upload_file_table DROP COLUMN is_finish;
@@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE upload_file_table ADD COLUMN is_finish BOOLEAN NOT NULL DEFAULT FALSE;
@@ -64,6 +64,7 @@ diesel::table! {
num_chunk -> Integer,
upload_id -> Text,
created_at -> BigInt,
is_finish -> Bool,
}
}
@@ -9,6 +9,7 @@ use tokio::io::AsyncReadExt;
/// In Amazon S3, the minimum chunk size for multipart uploads is 5 MB,except for the last part,
/// which can be smaller.(https://docs.aws.amazon.com/AmazonS3/latest/userguide/qfacts.html)
pub const MIN_CHUNK_SIZE: usize = 5 * 1024 * 1024; // Minimum Chunk Size 5 MB
#[derive(Debug, Clone)]
pub struct ChunkedBytes {
pub data: Bytes,
pub chunk_size: i32,
@@ -28,8 +29,11 @@ impl Display for ChunkedBytes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"ChunkedBytes: chunk_size: {}, offsets: {:?}, current_offset: {}",
self.chunk_size, self.offsets, self.current_offset
"data:{}, chunk_size:{}, num chunk:{}, offset:{}",
self.data.len(),
self.chunk_size,
self.offsets.len(),
self.current_offset
)
}
}
@@ -21,7 +21,7 @@ pub trait StorageService: Send + Sync {
upload_immediately: bool,
) -> Result<(CreatedUpload, Option<FileProgressReceiver>), FlowyError>;
async fn start_upload(&self, chunks: &ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError>;
async fn start_upload(&self, chunks: ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError>;
async fn resume_upload(
&self,
@@ -32,8 +32,9 @@ pub trait StorageService: Send + Sync {
async fn subscribe_file_progress(
&self,
parent_idr: &str,
file_id: &str,
) -> Result<FileProgressReceiver, FlowyError>;
) -> Result<Option<FileProgressReceiver>, FlowyError>;
}
pub struct FileProgressReceiver {
+57 -55
View File
@@ -2,8 +2,8 @@ use crate::file_cache::FileTempStorage;
use crate::notification::{make_notification, StorageNotification};
use crate::sqlite_sql::{
batch_select_upload_file, delete_upload_file, insert_upload_file, insert_upload_part,
select_upload_file, select_upload_parts, update_upload_file_upload_id, UploadFilePartTable,
UploadFileTable,
is_upload_completed, select_upload_file, select_upload_parts, update_upload_file_completed,
update_upload_file_upload_id, UploadFilePartTable, UploadFileTable,
};
use crate::uploader::{FileUploader, FileUploaderRunner, Signal, UploadTask, UploadTaskQueue};
use async_trait::async_trait;
@@ -117,9 +117,13 @@ impl StorageManager {
pub async fn subscribe_file_state(
&self,
parent_dir: &str,
file_id: &str,
) -> Result<FileProgressReceiver, FlowyError> {
self.storage_service.subscribe_file_progress(file_id).await
) -> Result<Option<FileProgressReceiver>, FlowyError> {
self
.storage_service
.subscribe_file_progress(parent_dir, file_id)
.await
}
pub async fn get_file_state(&self, file_id: &str) -> Option<FileUploadState> {
@@ -314,7 +318,7 @@ impl StorageService for StorageServiceImpl {
}
}
async fn start_upload(&self, chunks: &ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError> {
async fn start_upload(&self, chunks: ChunkedBytes, record: &BoxAny) -> Result<(), FlowyError> {
let file_record = record.downcast_ref::<UploadFileTable>().ok_or_else(|| {
FlowyError::internal().with_context("failed to downcast record to UploadFileTable")
})?;
@@ -341,34 +345,16 @@ impl StorageService for StorageServiceImpl {
file_id: &str,
) -> Result<(), FlowyError> {
// Gathering the upload record and parts from the sqlite database.
let record = {
let mut conn = self
.user_service
.sqlite_connection(self.user_service.user_id()?)?;
conn.immediate_transaction(|conn| {
Ok::<_, FlowyError>(
// When resuming an upload, check if the upload_id is empty.
// If the upload_id is empty, the upload has likely not been created yet.
// If the upload_id is not empty, verify which parts have already been uploaded.
select_upload_file(conn, workspace_id, parent_dir, file_id)?.map(|record| {
if record.upload_id.is_empty() {
(record, vec![])
} else {
let parts = select_upload_parts(conn, &record.upload_id).unwrap_or_default();
(record, parts)
}
}),
)
})?
};
let mut conn = self
.user_service
.sqlite_connection(self.user_service.user_id()?)?;
if let Some((upload_file, parts)) = record {
if let Some(upload_file) = select_upload_file(&mut conn, workspace_id, parent_dir, file_id)? {
resume_upload(
&self.cloud_service,
&self.user_service,
&self.temp_storage,
upload_file,
parts,
self.progress_notifiers.clone(),
)
.await?;
@@ -380,18 +366,32 @@ impl StorageService for StorageServiceImpl {
async fn subscribe_file_progress(
&self,
parent_idr: &str,
file_id: &str,
) -> Result<FileProgressReceiver, FlowyError> {
) -> Result<Option<FileProgressReceiver>, FlowyError> {
trace!("[File]: subscribe file progress: {}", file_id);
let is_completed = {
let mut conn = self
.user_service
.sqlite_connection(self.user_service.user_id()?)?;
let workspace_id = self.user_service.workspace_id()?;
is_upload_completed(&mut conn, &workspace_id, parent_idr, file_id).unwrap_or(false)
};
if is_completed {
return Ok(None);
}
let (notifier, receiver) = ProgressNotifier::new();
let receiver = FileProgressReceiver {
rx: receiver,
file_id: file_id.to_string(),
};
self
.progress_notifiers
.insert(file_id.to_string(), notifier);
Ok(receiver)
Ok(Some(receiver))
}
}
@@ -421,6 +421,7 @@ async fn create_upload_record(
chunk_size: chunked_bytes.chunk_size,
num_chunk: chunked_bytes.offsets.len() as i32,
created_at: timestamp(),
is_finish: false,
};
Ok((chunked_bytes, record))
}
@@ -430,10 +431,29 @@ async fn start_upload(
cloud_service: &Arc<dyn StorageCloudService>,
user_service: &Arc<dyn StorageUserService>,
temp_storage: &Arc<FileTempStorage>,
chunked_bytes: &ChunkedBytes,
mut chunked_bytes: ChunkedBytes,
upload_file: &UploadFileTable,
progress_notifiers: Arc<DashMap<String, ProgressNotifier>>,
) -> FlowyResult<()> {
// 4. gather existing completed parts
let mut conn = user_service.sqlite_connection(user_service.user_id()?)?;
let mut completed_parts = select_upload_parts(&mut conn, &upload_file.upload_id)
.unwrap_or_default()
.into_iter()
.map(|part| CompletedPartRequest {
e_tag: part.e_tag,
part_number: part.part_num,
})
.collect::<Vec<_>>();
let upload_offset = completed_parts.len() as i32;
chunked_bytes.set_current_offset(upload_offset);
info!(
"[File] start upload: workspace: {}, parent_dir: {}, file_id: {}, chunk: {}",
upload_file.workspace_id, upload_file.parent_dir, upload_file.file_id, chunked_bytes,
);
let mut upload_file = upload_file.clone();
if upload_file.upload_id.is_empty() {
// 1. create upload
@@ -489,25 +509,10 @@ async fn start_upload(
let total_parts = chunked_bytes.iter().count();
let iter = chunked_bytes.iter().enumerate();
let mut conn = user_service.sqlite_connection(user_service.user_id()?)?;
// 4. gather existing completed parts
let mut completed_parts = select_upload_parts(&mut conn, &upload_file.upload_id)
.unwrap_or_default()
.into_iter()
.map(|part| CompletedPartRequest {
e_tag: part.e_tag,
part_number: part.part_num,
})
.collect::<Vec<_>>();
// when there are any existing parts, skip those parts by setting the current offset.
let offset = completed_parts.len();
for (index, chunk_bytes) in iter {
let part_number = offset + index + 1;
let part_number = upload_offset + index as i32 + 1;
trace!(
"[File] {} uploading part: {}, len:{}KB",
"[File] {} uploading {}th part, size:{}KB",
upload_file.file_id,
part_number,
chunk_bytes.len() / 1000,
@@ -585,7 +590,6 @@ async fn resume_upload(
user_service: &Arc<dyn StorageUserService>,
temp_storage: &Arc<FileTempStorage>,
upload_file: UploadFileTable,
parts: Vec<UploadFilePartTable>,
progress_notifiers: Arc<DashMap<String, ProgressNotifier>>,
) -> FlowyResult<()> {
trace!(
@@ -597,14 +601,13 @@ async fn resume_upload(
);
match ChunkedBytes::from_file(&upload_file.local_file_path, MIN_CHUNK_SIZE as i32).await {
Ok(mut chunked_bytes) => {
Ok(chunked_bytes) => {
// When there were any parts already uploaded, skip those parts by setting the current offset.
chunked_bytes.set_current_offset(parts.len() as i32);
start_upload(
cloud_service,
user_service,
temp_storage,
&chunked_bytes,
chunked_bytes,
&upload_file,
progress_notifiers,
)
@@ -676,7 +679,7 @@ async fn complete_upload(
progress_notifiers: &Arc<DashMap<String, ProgressNotifier>>,
) -> Result<(), FlowyError> {
trace!(
"[File]: completing file upload: {}, part: {}",
"[File]: completing file upload: {}, num parts: {}",
upload_file.file_id,
parts.len()
);
@@ -693,7 +696,7 @@ async fn complete_upload(
Ok(_) => {
info!("[File] completed upload file: {}", upload_file.file_id);
if let Some(mut notifier) = progress_notifiers.get_mut(&upload_file.file_id) {
trace!("[File]: notify upload finished");
info!("[File]: notify upload:{} finished", upload_file.file_id);
notifier
.notify(FileUploadState::Finished {
file_id: upload_file.file_id.clone(),
@@ -701,9 +704,8 @@ async fn complete_upload(
.await;
}
trace!("[File] delete upload record from sqlite");
let conn = user_service.sqlite_connection(user_service.user_id()?)?;
delete_upload_file(conn, &upload_file.upload_id)?;
update_upload_file_completed(conn, &upload_file.upload_id)?;
if let Err(err) = temp_storage
.delete_temp_file(&upload_file.local_file_path)
.await
@@ -21,6 +21,7 @@ pub struct UploadFileTable {
pub num_chunk: i32,
pub upload_id: String,
pub created_at: i64,
pub is_finish: bool,
}
#[derive(Queryable, Insertable, AsChangeset, Identifiable, Debug)]
@@ -87,6 +88,55 @@ pub fn update_upload_file_upload_id(
Ok(())
}
pub fn update_upload_file_completed(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> {
diesel::update(
upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)),
)
.set(upload_file_table::is_finish.eq(true))
.execute(&mut *conn)?;
Ok(())
}
pub fn is_upload_completed(
conn: &mut SqliteConnection,
workspace_id: &str,
parent_dir: &str,
file_id: &str,
) -> FlowyResult<bool> {
let result = upload_file_table::dsl::upload_file_table
.filter(
upload_file_table::workspace_id
.eq(workspace_id)
.and(upload_file_table::parent_dir.eq(parent_dir))
.and(upload_file_table::file_id.eq(file_id))
.and(upload_file_table::is_finish.eq(true)),
)
.first::<UploadFileTable>(conn)
.optional()?;
Ok(result.is_some())
}
pub fn delete_upload_file(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> {
conn.immediate_transaction(|conn| {
diesel::delete(
upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)),
)
.execute(&mut *conn)?;
if let Err(err) = diesel::delete(
upload_file_part::dsl::upload_file_part.filter(upload_file_part::upload_id.eq(upload_id)),
)
.execute(&mut *conn)
{
warn!("Failed to delete upload parts: {:?}", err)
}
Ok::<_, FlowyError>(())
})?;
Ok(())
}
pub fn insert_upload_part(
mut conn: DBConnection,
upload_part: &UploadFilePartTable,
@@ -147,24 +197,3 @@ pub fn select_upload_file(
.optional()?;
Ok(result)
}
pub fn delete_upload_file(mut conn: DBConnection, upload_id: &str) -> FlowyResult<()> {
conn.immediate_transaction(|conn| {
diesel::delete(
upload_file_table::dsl::upload_file_table.filter(upload_file_table::upload_id.eq(upload_id)),
)
.execute(&mut *conn)?;
if let Err(err) = diesel::delete(
upload_file_part::dsl::upload_file_part.filter(upload_file_part::upload_id.eq(upload_id)),
)
.execute(&mut *conn)
{
warn!("Failed to delete upload parts: {:?}", err)
}
Ok::<_, FlowyError>(())
})?;
Ok(())
}
@@ -164,7 +164,11 @@ impl FileUploader {
mut retry_count,
} => {
let record = BoxAny::new(record);
if let Err(err) = self.storage_service.start_upload(&chunks, &record).await {
if let Err(err) = self
.storage_service
.start_upload(chunks.clone(), &record)
.await
{
if err.is_file_limit_exceeded() {
error!("Failed to upload file: {}", err);
self.disable_storage_write();
@@ -175,5 +175,6 @@ pub async fn create_upload_file_record(
chunk_size: MIN_CHUNK_SIZE as i32,
num_chunk: chunked_bytes.offsets.len() as i32,
created_at: chrono::Utc::now().timestamp(),
is_finish: false,
}
}
@@ -53,12 +53,9 @@ pub struct UserProfilePB {
pub encryption_type: EncryptionTypePB,
#[pb(index = 10)]
pub workspace_id: String,
#[pb(index = 11)]
pub stability_ai_key: String,
#[pb(index = 12)]
#[pb(index = 11)]
pub ai_model: AIModelPB,
}
@@ -90,7 +87,6 @@ impl From<UserProfile> for UserProfilePB {
authenticator: user_profile.authenticator.into(),
encryption_sign,
encryption_type: encryption_ty,
workspace_id: user_profile.workspace_id,
stability_ai_key: user_profile.stability_ai_key,
ai_model: AIModelPB::from_str(&user_profile.ai_model).unwrap_or_default(),
}