diff --git a/client/lib/integrations/download_manager.dart b/client/lib/integrations/download_manager.dart index 5871d2a1..4508c8b5 100644 --- a/client/lib/integrations/download_manager.dart +++ b/client/lib/integrations/download_manager.dart @@ -6,6 +6,7 @@ import 'package:path/path.dart' as p; import 'package:phylum/integrations/directories.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; import 'package:phylum/libphylum/requests/resource_contents_request.dart'; import 'package:state_notifier/state_notifier.dart'; @@ -40,18 +41,13 @@ class DownloadManager extends StateNotifier { task._status.addListener(l); state = state.copyWith(running: [...state.running, task]); - final apiResponse = await account.api.sendRequestRaw(ResourceContentsRequest(r.id)); - if (apiResponse.error != null) { - final msg = switch (apiResponse.error) { - const ApiClosedError() => 'Api closed', - const ApiOfflineError() => 'Cannot reach server', - ApiUnknownError(ex: dynamic ex) => 'Unknown Error - $ex', - ApiRequestError(err: PhylumApiErrorResponse err) => 'Request Error - ${err.message}', - _ => '', - }; - task._status.value = DownloadStatusError(msg); - return; - } else { + try { + final response = await account.apiClient.dispatchRequestRaw(ResourceContentsRequest(r.id)); + if (response.statusCode < 200 || response.statusCode > 300) { + final error = PhylumApiErrorResponse.fromResponseString(await response.bodyString()); + task._status.value = DownloadStatusError(error.message); + return; + } final output = _createDownloadFile(PhylumDirectories.instance.downloads!, r.name); if (output == null) { task._status.value = const DownloadStatusError('Unable to open output file'); @@ -59,10 +55,9 @@ class DownloadManager extends StateNotifier { } final sink = output.openWrite(); - final httpResponse = apiResponse.result!; - final length = httpResponse.contentLength ?? r.size; + final length = response.contentLength ?? r.size; int received = 0; - final stream = httpResponse.stream.map((s) { + final stream = response.stream.map((s) { received += s.length; task._status.value = DownloadStatusRunning(received, length); return s; @@ -73,6 +68,10 @@ class DownloadManager extends StateNotifier { task._status.value = DownloadStatusError(err.toString()); output.deleteSync(); }).whenComplete(() => task._status.value = DownloadStatusFinished(output.path)); + } on SocketException { + task._status.value = const DownloadStatusError('Cannot reach server'); + } catch (e) { + task._status.value = DownloadStatusError('Unknown Error - $e'); } } diff --git a/client/lib/libphylum/actions/action_resource_delete.dart b/client/lib/libphylum/actions/action_resource_delete.dart index 15d0bcfe..9e8aa383 100644 --- a/client/lib/libphylum/actions/action_resource_delete.dart +++ b/client/lib/libphylum/actions/action_resource_delete.dart @@ -4,8 +4,9 @@ import 'package:drift/drift.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; -class ResourceDeleteAction extends ApiAction with JsonApiAction { +class ResourceDeleteAction extends ApiAction with JsonApiAction { static const actionName = 'resourceDelete'; @override String get name => actionName; diff --git a/client/lib/libphylum/actions/action_resource_mkdir.dart b/client/lib/libphylum/actions/action_resource_mkdir.dart index 4bd2486c..46725d83 100644 --- a/client/lib/libphylum/actions/action_resource_mkdir.dart +++ b/client/lib/libphylum/actions/action_resource_mkdir.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/libphylum/phylum_datastore.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; -class ResourceMkdirAction extends ApiAction with JsonApiAction { +class ResourceMkdirAction extends ApiAction with JsonApiAction { static const actionName = 'resourceMkdir'; @override String get name => actionName; @@ -15,9 +15,6 @@ class ResourceMkdirAction extends ApiAction with JsonApiAction { @override String get endpoint => '/api/v1/resources/mkdir/$id'; - @override - get tag => resourceSummaryResponse; - final String id; final String parent; final String resourceName; @@ -65,4 +62,11 @@ class ResourceMkdirAction extends ApiAction with JsonApiAction { 'resourceName': resourceName, 'parent': parent, }; + + @override + FutureOr processResponse(PhylumAccount account, ApiResponse response) async { + if (response is ApiSuccessResponse) { + await account.resourceRepository.parseResourceSummaryResponse(response.result); + } + } } diff --git a/client/lib/libphylum/actions/action_resource_move.dart b/client/lib/libphylum/actions/action_resource_move.dart index cf2eb8b6..abcf777a 100644 --- a/client/lib/libphylum/actions/action_resource_move.dart +++ b/client/lib/libphylum/actions/action_resource_move.dart @@ -4,9 +4,9 @@ import 'package:drift/drift.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/libphylum/phylum_datastore.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; -class ResourceMoveAction extends ApiAction with JsonApiAction { +class ResourceMoveAction extends ApiAction with JsonApiAction { static const actionName = 'resourceMove'; @override String get name => actionName; @@ -17,9 +17,6 @@ class ResourceMoveAction extends ApiAction with JsonApiAction { @override String get endpoint => '/api/v1/resources/move/$id'; - @override - get tag => resourceSummaryResponse; - final String id; final String parent; final String? oldParent; @@ -65,4 +62,11 @@ class ResourceMoveAction extends ApiAction with JsonApiAction { 'oldParent': oldParent, 'description': description, }; + + @override + FutureOr processResponse(PhylumAccount account, ApiResponse response) async { + if (response is ApiSuccessResponse) { + await account.resourceRepository.parseResourceSummaryResponse(response.result); + } + } } diff --git a/client/lib/libphylum/actions/action_resource_rename.dart b/client/lib/libphylum/actions/action_resource_rename.dart index ba2c91f7..87178b70 100644 --- a/client/lib/libphylum/actions/action_resource_rename.dart +++ b/client/lib/libphylum/actions/action_resource_rename.dart @@ -4,9 +4,9 @@ import 'package:drift/drift.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/libphylum/phylum_datastore.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; -class ResourceRenameAction extends ApiAction with JsonApiAction { +class ResourceRenameAction extends ApiAction with JsonApiAction { static const actionName = 'resourceRename'; @override String get name => actionName; @@ -17,9 +17,6 @@ class ResourceRenameAction extends ApiAction with JsonApiAction { @override String get endpoint => '/api/v1/resources/rename/$id'; - @override - get tag => resourceSummaryResponse; - final String id; final String newName; final String oldName; @@ -71,4 +68,11 @@ class ResourceRenameAction extends ApiAction with JsonApiAction { 'newName': newName, 'oldName': oldName, }; + + @override + FutureOr processResponse(PhylumAccount account, ApiResponse response) async { + if (response is ApiSuccessResponse) { + await account.resourceRepository.parseResourceSummaryResponse(response.result); + } + } } diff --git a/client/lib/libphylum/actions/action_resource_upload.dart b/client/lib/libphylum/actions/action_resource_upload.dart index 10da2c15..4e76c58f 100644 --- a/client/lib/libphylum/actions/action_resource_upload.dart +++ b/client/lib/libphylum/actions/action_resource_upload.dart @@ -6,10 +6,10 @@ import 'package:http_parser/http_parser.dart'; import 'package:mime/mime.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/libphylum/phylum_datastore.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; import 'package:uuid/uuid.dart'; -class ResourceUploadAction extends ApiAction with FileUploadApiAction { +class ResourceUploadAction extends ApiAction with FileUploadApiAction { static const actionName = 'resourceUpload'; @override String get name => actionName; @@ -20,9 +20,6 @@ class ResourceUploadAction extends ApiAction with FileUploadApiAc @override String get endpoint => '/api/v1/resources/upload/$id'; - @override - get tag => resourceSummaryResponse; - @override Future get file => MultipartFile.fromPath('contents', path, filename: resourceName, contentType: MediaType.parse(contentType)); @@ -96,4 +93,11 @@ class ResourceUploadAction extends ApiAction with FileUploadApiAc 'name': resourceName, }; } + + @override + FutureOr processResponse(PhylumAccount account, ApiResponse response) async { + if (response is ApiSuccessResponse) { + await account.resourceRepository.parseResourceSummaryResponse(response.result); + } + } } diff --git a/client/lib/libphylum/phylum_account.dart b/client/lib/libphylum/phylum_account.dart index 8b1f5c0b..f91b9229 100644 --- a/client/lib/libphylum/phylum_account.dart +++ b/client/lib/libphylum/phylum_account.dart @@ -1,43 +1,28 @@ -import 'dart:convert'; - import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; -import 'package:phylum/libphylum/phylum_datastore.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; import 'package:phylum/libphylum/repositories/resource_repository.dart'; +import 'package:phylum/util/logging.dart'; const _persistKeyAccessToken = 'accessToken'; const _persistKeyUserEmail = 'userEmail'; const _persistKeyUserName = 'userName'; const _persistKeyUserHome = 'userHome'; -typedef PhylumApiResponse = Map; - -class PhylumApiErrorResponse { - final String code; - final String message; - - PhylumApiErrorResponse._({required this.code, required this.message}); - - factory PhylumApiErrorResponse.fromResponseString(String response) { - final map = (jsonDecode(response) as Map?) ?? const {}; - return PhylumApiErrorResponse._(code: map['code'] ?? 'server_error', message: map['msg'] ?? 'Server Error'); - } -} - -Map _tansformResponse(String response) => (jsonDecode(response) as Map?) ?? const {}; - class PhylumAccount extends Account { late final db = AppDatabase(id: id); late final resourceRepository = ResourceRepository(account: this); - final datastore = PhylumDatastore(); - final actionQueue = ApiActionQueue(); + + late final _dispatcher = HttpClientDispatcher(); + @override + Dispatcher get dispatcher => _dispatcher; String? get accessToken => getPersisted(_persistKeyAccessToken); set _accessToken(String? value) { if (value != accessToken) { persist(_persistKeyAccessToken, value); } - api.setHeader('Authorization', 'bearer $value'); + apiClient.setHeader('Authorization', 'bearer $value'); } String get userEmail => getPersisted(_persistKeyUserEmail); @@ -55,16 +40,9 @@ class PhylumAccount extends Account initialize() async { @@ -72,8 +50,16 @@ class PhylumAccount extends Account) { + if (errorResponse.err.code == "credentials_invalid") { + logger.i('Invalid Credentials - Logging out'); + logOut(); + l?.call(); + } + } + }); } @override @@ -82,10 +68,6 @@ class PhylumAccount extends Account addAction(ApiAction action) async { - return actionQueue.addAction(action); - } - static Future createFromLoginResponse(Uri serverUri, Map response) async { final account = PhylumAccount.create(serverUri: serverUri); await account.initialized; diff --git a/client/lib/libphylum/phylum_api_response.dart b/client/lib/libphylum/phylum_api_response.dart new file mode 100644 index 00000000..d54f9440 --- /dev/null +++ b/client/lib/libphylum/phylum_api_response.dart @@ -0,0 +1,27 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:offtheline/offtheline.dart'; + +typedef PhylumApiResponse = Map; + +class PhylumApiErrorResponse { + final String code; + final String message; + + PhylumApiErrorResponse._({required this.code, required this.message}); + + factory PhylumApiErrorResponse.fromResponseString(String response) { + final map = (jsonDecode(response) as Map?) ?? const {}; + return PhylumApiErrorResponse._(code: map['code'] ?? 'server_error', message: map['msg'] ?? 'Server Error'); + } +} + +Future> transformApiResponse(ApiRequest _, StreamedResponse response) async { + final body = await response.bodyString(); + if (response.statusCode >= 200 && response.statusCode < 300) { + final apiResponse = (jsonDecode(body) as Map?) ?? const {}; + return ApiSuccessResponse(apiResponse); + } + return ApiErrorResponse(statusCode: response.statusCode, err: PhylumApiErrorResponse.fromResponseString(body)); +} diff --git a/client/lib/libphylum/phylum_datastore.dart b/client/lib/libphylum/phylum_datastore.dart deleted file mode 100644 index 21afa976..00000000 --- a/client/lib/libphylum/phylum_datastore.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart'; -import 'package:offtheline/offtheline.dart'; -import 'package:phylum/libphylum/db/db.dart'; -import 'package:phylum/libphylum/phylum_account.dart'; -import 'package:phylum/util/logging.dart'; - -const resourceDetailsResponse = 'resourceDetails'; -const resourceSummaryResponse = 'resourceSummary'; - -class PhylumDatastore with AccountListener { - @override - Future initialize(PhylumAccount account) async { - super.initialize(account); - account.api.addResponseListener((data, tag) => account.db.transaction(() async => await _processResponse(data, tag, account.db))); - account.api.addErrorResponseListener((errorResponse) { - if (errorResponse.code == "credentials_invalid") { - logger.i('Invalid Credentials - Logging out'); - account.logOut(); - } - }); - } - - FutureOr _processResponse(PhylumApiResponse data, dynamic tag, AppDatabase db) { - switch (tag) { - case resourceDetailsResponse: - return parseResourceDetails(data, db); - case resourceSummaryResponse: - return parseResourceSummary(data, db); - } - } - - Future parseResourceDetails(Map data, AppDatabase db) async { - final details = parseResourceSummary(data['metadata'], db).copyWith(lastFetch: Value(DateTime.now())); - final existing = Set.from(await db.managers.resources.filter((f) => f.parent.id.equals(data['metadata']['id'])).map((r) => r.id).get()); - final children = data.containsKey('children') - ? (data['children'] as List).cast().map((c) { - existing.remove(c['id']); - return parseResourceSummary(c.cast(), db); - }) - : []; - await db.resources.deleteWhere((o) => o.id.isIn(List.from(existing))); - return db.batch((batch) { - batch.insertAll(db.resources, [details, ...children], mode: InsertMode.insertOrReplace); - }); - } - - ResourcesCompanion parseResourceSummary(Map data, AppDatabase db) { - return ResourcesCompanion.insert( - id: data['id'], - parent: Value(data['parent']), - name: data['name'], - dir: data['dir'], - modified: DateTime.parse(data['modified']), - size: data['size'], - etag: data['etag'], - contentType: data['ctype'] ?? '', - ); - } -} diff --git a/client/lib/libphylum/repositories/resource_repository.dart b/client/lib/libphylum/repositories/resource_repository.dart index a0c6739a..86957253 100644 --- a/client/lib/libphylum/repositories/resource_repository.dart +++ b/client/lib/libphylum/repositories/resource_repository.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:offtheline/offtheline.dart'; import 'package:phylum/libphylum/db/db.dart'; import 'package:phylum/libphylum/phylum_account.dart'; +import 'package:phylum/libphylum/phylum_api_response.dart'; import 'package:phylum/libphylum/requests/resource_detail_request.dart'; class ResourceRepository { @@ -9,8 +10,13 @@ class ResourceRepository { ResourceRepository({required this.account}); - Future?> requestResource(String id) { - return account.api.sendRequest(ResourceDetailRequest(id)).then((r) => r.error); + Future?> requestResource(String id) async { + final response = await account.apiClient.sendRequest(ResourceDetailRequest(id)); + if (response is ApiSuccessResponse) { + await parseResourceDetailsResponse(response.result); + return null; + } + return response; } Future getResource(String id) { @@ -61,4 +67,39 @@ class ResourceRepository { Future deleteResource(String id) { return account.db.resources.deleteWhere((f) => f.id.equals(id)); } + + Future parseResourceDetailsResponse(Map data) async { + final db = account.db; + final details = parseResourceSummaryObject(data['metadata']).copyWith(lastFetch: Value(DateTime.now())); + final existing = Set.from(await db.managers.resources.filter((f) => f.parent.id.equals(data['metadata']['id'])).map((r) => r.id).get()); + final children = data.containsKey('children') + ? (data['children'] as List).cast().map((c) { + existing.remove(c['id']); + return parseResourceSummaryObject(c.cast()); + }) + : []; + return db.transaction(() async { + await db.resources.deleteWhere((o) => o.id.isIn(List.from(existing))); + await db.batch((batch) { + batch.insertAll(db.resources, [details, ...children], mode: InsertMode.insertOrReplace); + }); + }); + } + + Future parseResourceSummaryResponse(Map data) async { + return account.db.into(account.db.resources).insert(parseResourceSummaryObject(data), mode: InsertMode.insertOrReplace); + } + + ResourcesCompanion parseResourceSummaryObject(Map data) { + return ResourcesCompanion.insert( + id: data['id'], + parent: Value(data['parent']), + name: data['name'], + dir: data['dir'], + modified: DateTime.parse(data['modified']), + size: data['size'], + etag: data['etag'], + contentType: data['ctype'] ?? '', + ); + } } diff --git a/client/lib/libphylum/requests/resource_detail_request.dart b/client/lib/libphylum/requests/resource_detail_request.dart index b1ffb699..7c74d945 100644 --- a/client/lib/libphylum/requests/resource_detail_request.dart +++ b/client/lib/libphylum/requests/resource_detail_request.dart @@ -1,11 +1,7 @@ import 'package:http/http.dart'; import 'package:offtheline/offtheline.dart'; -import 'package:phylum/libphylum/phylum_datastore.dart'; class ResourceDetailRequest extends ApiRequest { - @override - get tag => resourceDetailsResponse; - final String resourceId; const ResourceDetailRequest(this.resourceId); diff --git a/client/lib/ui/explorer/explorer_actions.dart b/client/lib/ui/explorer/explorer_actions.dart index eef23172..80a29820 100644 --- a/client/lib/ui/explorer/explorer_actions.dart +++ b/client/lib/ui/explorer/explorer_actions.dart @@ -70,7 +70,7 @@ class ExplorerActions extends StatelessWidget { final account = context.read(); final selected = context.read().selected; final items = selected.map((r) { - final uri = account.api.createUriBuilder('/open'); + final uri = account.apiClient.createUriBuilder('/open'); uri.queryParameters['id'] = r.id; if (i.cut) { uri.queryParameters['cut'] = 'y'; @@ -86,7 +86,7 @@ class ExplorerActions extends StatelessWidget { // TODO: Move to top-level nav final account = context.read(); final folderId = context.read().folderId; - final openUri = account.api.createUri('/open'); + final openUri = account.apiClient.createUri('/open'); final clipboard = SystemClipboard.instance; if (clipboard == null) { return; diff --git a/client/lib/ui/preview/resource_preview.dart b/client/lib/ui/preview/resource_preview.dart index ca727fd4..207cb241 100644 --- a/client/lib/ui/preview/resource_preview.dart +++ b/client/lib/ui/preview/resource_preview.dart @@ -77,11 +77,11 @@ class _ResourcePreviewState extends State { } Widget buildImagePreview(Resource r) { - final api = context.read().api; + final api = context.read().apiClient; return SizedBox( width: 800, child: Image.network( - context.read().api.createUri('/api/v1/resources/contents/${r.id}').toString(), + context.read().apiClient.createUri('/api/v1/resources/contents/${r.id}').toString(), headers: api.requestHeaders, ), ); diff --git a/client/pubspec.lock b/client/pubspec.lock index da186a5f..f7dd9ad1 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -577,11 +577,11 @@ packages: dependency: "direct main" description: path: "." - ref: ad3d1e804990bcb9ca65a1e5d003b1cb680908e0 - resolved-ref: ad3d1e804990bcb9ca65a1e5d003b1cb680908e0 + ref: "7e062f421d95e48234bfd040ba20714f11e8c509" + resolved-ref: "7e062f421d95e48234bfd040ba20714f11e8c509" url: "https://codeberg.org/shroff/offtheline.git" source: git - version: "0.14.0" + version: "0.15.0" open_file: dependency: "direct main" description: diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 51307fcf..883a2f7f 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: offtheline: git: url: https://codeberg.org/shroff/offtheline.git - ref: ad3d1e804990bcb9ca65a1e5d003b1cb680908e0 + ref: 7e062f421d95e48234bfd040ba20714f11e8c509 open_file: path: path_provider: