mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
background upload plugin
add schemas sync variants formatting initial implementation use existing db, wip move to separate folder fix table definitions wip wiring it up
This commit is contained in:
@@ -1,15 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/network_capability_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
@@ -17,16 +13,13 @@ import 'package:immich_mobile/platform/background_worker_api.g.dart';
|
||||
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/repositories/file_media.repository.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/services/auth.service.dart';
|
||||
import 'package:immich_mobile/services/localization.service.dart';
|
||||
import 'package:immich_mobile/services/upload.service.dart';
|
||||
import 'package:immich_mobile/utils/bootstrap.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_options.dart';
|
||||
@@ -96,23 +89,10 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
loadTranslations(),
|
||||
workerManagerPatch.init(dynamicSpawning: true),
|
||||
_ref?.read(authServiceProvider).setOpenApiServiceEndpoint(),
|
||||
// Initialize the file downloader
|
||||
FileDownloader().configure(
|
||||
globalConfig: [
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
(Config.holdingQueue, (6, 6, 3)),
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
(Config.runInForegroundIfFileLargerThan, 256),
|
||||
],
|
||||
),
|
||||
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
|
||||
FileDownloader().trackTasks(),
|
||||
_ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
|
||||
].nonNulls,
|
||||
);
|
||||
|
||||
configureFileDownloaderNotifications();
|
||||
|
||||
// Notify the host that the background worker service has been initialized and is ready to use
|
||||
unawaited(_backgroundHostApi.onInitialized());
|
||||
} catch (error, stack) {
|
||||
@@ -130,7 +110,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
||||
return;
|
||||
}
|
||||
await _handleBackup();
|
||||
await uploadApi.refresh();
|
||||
} catch (error, stack) {
|
||||
_logger.severe("Failed to complete Android background processing", error, stack);
|
||||
} finally {
|
||||
@@ -150,13 +130,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
_logger.warning("Remote sync did not complete successfully, skipping backup");
|
||||
return;
|
||||
}
|
||||
|
||||
final backupFuture = _handleBackup();
|
||||
if (maxSeconds != null) {
|
||||
await backupFuture.timeout(Duration(seconds: maxSeconds - 1), onTimeout: () {});
|
||||
} else {
|
||||
await backupFuture;
|
||||
}
|
||||
await uploadApi.refresh();
|
||||
} catch (error, stack) {
|
||||
_logger.severe("Failed to complete iOS background upload", error, stack);
|
||||
} finally {
|
||||
@@ -224,39 +198,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleBackup() async {
|
||||
await runZonedGuarded(
|
||||
() async {
|
||||
if (_isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isBackupEnabled) {
|
||||
_logger.info("Backup is disabled. Skipping backup routine");
|
||||
return;
|
||||
}
|
||||
|
||||
final currentUser = _ref?.read(currentUserProvider);
|
||||
if (currentUser == null) {
|
||||
_logger.warning("No current user found. Skipping backup from background");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isIOS) {
|
||||
return _ref?.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id);
|
||||
}
|
||||
|
||||
final networkCapabilities = await _ref?.read(connectivityApiProvider).getCapabilities() ?? [];
|
||||
return _ref
|
||||
?.read(uploadServiceProvider)
|
||||
.startBackupWithHttpClient(currentUser.id, networkCapabilities.isUnmetered, _cancellationToken);
|
||||
},
|
||||
(error, stack) {
|
||||
dPrint(() => "Error in backup zone $error, $stack");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _syncAssets({Duration? hashTimeout}) async {
|
||||
await _ref?.read(backgroundSyncProvider).syncLocal();
|
||||
if (_isCleanedUp) {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:drift/drift.dart' hide Index;
|
||||
|
||||
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_upload_tasks_local_id ON upload_task_entity(local_id);')
|
||||
@TableIndex.sql('CREATE INDEX idx_upload_tasks_asset_data ON upload_task_entity(status, priority DESC, created_at);')
|
||||
class UploadTaskEntity extends Table {
|
||||
const UploadTaskEntity();
|
||||
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get attempts => integer()();
|
||||
DateTimeColumn get createdAt => dateTime()();
|
||||
TextColumn get filePath => text()();
|
||||
BoolColumn get isLivePhoto => boolean().nullable()();
|
||||
IntColumn get lastError => integer().nullable()();
|
||||
TextColumn get livePhotoVideoId => text().nullable()();
|
||||
TextColumn get localId => text()();
|
||||
IntColumn get method => integer()();
|
||||
RealColumn get priority => real()();
|
||||
DateTimeColumn get retryAfter => dateTime().nullable()();
|
||||
IntColumn get status => integer()();
|
||||
|
||||
@override
|
||||
bool get isStrict => true;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,78 @@
|
||||
CREATE TABLE upload_tasks
|
||||
(
|
||||
id integer primary key autoincrement,
|
||||
attempts integer not null,
|
||||
created_at integer not null,
|
||||
file_path text,
|
||||
is_live_photo integer,
|
||||
last_error integer,
|
||||
live_photo_video_id text,
|
||||
local_id text not null,
|
||||
method integer not null,
|
||||
priority real not null,
|
||||
retry_after integer,
|
||||
status integer not null
|
||||
);
|
||||
|
||||
CREATE TABLE upload_task_stats
|
||||
(
|
||||
pending_downloads integer not null,
|
||||
pending_uploads integer not null,
|
||||
queued_downloads integer not null,
|
||||
queued_uploads integer not null,
|
||||
failed_downloads integer not null,
|
||||
failed_uploads integer not null,
|
||||
completed_uploads integer not null,
|
||||
skipped_uploads integer not null
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_stats_insert
|
||||
BEFORE INSERT
|
||||
ON upload_tasks
|
||||
BEGIN
|
||||
UPDATE upload_task_stats
|
||||
SET pending_downloads = pending_downloads + (NEW.status = 0),
|
||||
queued_downloads = queued_downloads + (NEW.status = 1),
|
||||
failed_downloads = failed_downloads + (NEW.status = 2),
|
||||
pending_uploads = pending_uploads + (NEW.status = 3),
|
||||
queued_uploads = queued_uploads + (NEW.status = 4),
|
||||
failed_uploads = failed_uploads + (NEW.status = 5),
|
||||
completed_uploads = completed_uploads + (NEW.status = 6),
|
||||
skipped_uploads = skipped_uploads + (NEW.status = 7);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_stats_update
|
||||
BEFORE UPDATE OF status
|
||||
ON upload_tasks
|
||||
WHEN OLD.status != NEW.status
|
||||
BEGIN
|
||||
UPDATE upload_task_stats
|
||||
SET pending_downloads = pending_downloads - (OLD.status = 0) + (NEW.status = 0),
|
||||
queued_downloads = queued_downloads - (OLD.status = 1) + (NEW.status = 1),
|
||||
failed_downloads = failed_downloads - (OLD.status = 2) + (NEW.status = 2),
|
||||
pending_uploads = pending_uploads - (OLD.status = 3) + (NEW.status = 3),
|
||||
queued_uploads = queued_uploads - (OLD.status = 4) + (NEW.status = 4),
|
||||
failed_uploads = failed_uploads - (OLD.status = 5) + (NEW.status = 5),
|
||||
completed_uploads = completed_uploads - (OLD.status = 6) + (NEW.status = 6),
|
||||
skipped_uploads = skipped_uploads - (OLD.status = 7) + (NEW.status = 7);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_stats_delete
|
||||
BEFORE DELETE
|
||||
ON upload_tasks
|
||||
BEGIN
|
||||
UPDATE upload_task_stats
|
||||
SET pending_downloads = pending_downloads - (OLD.status = 0),
|
||||
queued_downloads = queued_downloads - (OLD.status = 1),
|
||||
failed_downloads = failed_downloads - (OLD.status = 2),
|
||||
pending_uploads = pending_uploads - (OLD.status = 3),
|
||||
queued_uploads = queued_uploads - (OLD.status = 4),
|
||||
failed_uploads = failed_uploads - (OLD.status = 5),
|
||||
completed_uploads = completed_uploads - (OLD.status = 6),
|
||||
skipped_uploads = skipped_uploads - (OLD.status = 7);
|
||||
END;
|
||||
|
||||
CREATE UNIQUE INDEX idx_upload_tasks_local_id ON upload_tasks (local_id, live_photo_video_id);
|
||||
CREATE INDEX idx_upload_tasks_asset_data ON upload_tasks (status, priority DESC, created_at);
|
||||
|
||||
@create: INSERT INTO upload_task_stats VALUES (0, 0, 0, 0, 0, 0, 0, 0);
|
||||
+1888
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/upload_tasks.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
|
||||
@@ -65,7 +66,10 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
||||
StoreEntity,
|
||||
TrashedLocalAssetEntity,
|
||||
],
|
||||
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
|
||||
include: {
|
||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||
'package:immich_mobile/infrastructure/entities/upload_tasks.drift',
|
||||
},
|
||||
)
|
||||
class Drift extends $Drift implements IDatabaseRepository {
|
||||
Drift([QueryExecutor? executor])
|
||||
@@ -95,7 +99,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 13;
|
||||
int get schemaVersion => 14;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -185,6 +189,16 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await m.createIndex(v13.idxTrashedLocalAssetChecksum);
|
||||
await m.createIndex(v13.idxTrashedLocalAssetAlbum);
|
||||
},
|
||||
from13To14: (m, v14) async {
|
||||
await m.createTable(UploadTasks(m.database));
|
||||
await m.createTable(UploadTaskStats(m.database));
|
||||
await m.create($drift0);
|
||||
await m.createTrigger(updateStatsInsert);
|
||||
await m.createTrigger(updateStatsUpdate);
|
||||
await m.createTrigger(updateStatsDelete);
|
||||
await m.createIndex(idxUploadTasksLocalId);
|
||||
await m.createIndex(idxUploadTasksAssetData);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
+126
-86
@@ -1,94 +1,109 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/upload_tasks.drift.dart'
|
||||
as i1;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||
as i2;
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||
as i3;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||
as i4;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
|
||||
as i5;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'
|
||||
as i6;
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'
|
||||
as i7;
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'
|
||||
as i8;
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/auth_user.entity.drift.dart'
|
||||
as i9;
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift.dart'
|
||||
as i10;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
|
||||
as i11;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
|
||||
as i12;
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'
|
||||
as i13;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||
as i14;
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||
as i15;
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||
as i16;
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/person.entity.drift.dart'
|
||||
as i17;
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/asset_face.entity.drift.dart'
|
||||
as i18;
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
|
||||
as i19;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart'
|
||||
as i20;
|
||||
import 'package:drift/internal/modular.dart' as i21;
|
||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||
as i21;
|
||||
import 'package:drift/internal/modular.dart' as i22;
|
||||
|
||||
abstract class $Drift extends i0.GeneratedDatabase {
|
||||
$Drift(i0.QueryExecutor e) : super(e);
|
||||
$DriftManager get managers => $DriftManager(this);
|
||||
late final i1.$UserEntityTable userEntity = i1.$UserEntityTable(this);
|
||||
late final i2.$RemoteAssetEntityTable remoteAssetEntity = i2
|
||||
late final i1.UploadTasks uploadTasks = i1.UploadTasks(this);
|
||||
late final i1.UploadTaskStats uploadTaskStats = i1.UploadTaskStats(this);
|
||||
late final i2.$UserEntityTable userEntity = i2.$UserEntityTable(this);
|
||||
late final i3.$RemoteAssetEntityTable remoteAssetEntity = i3
|
||||
.$RemoteAssetEntityTable(this);
|
||||
late final i3.$StackEntityTable stackEntity = i3.$StackEntityTable(this);
|
||||
late final i4.$LocalAssetEntityTable localAssetEntity = i4
|
||||
late final i4.$StackEntityTable stackEntity = i4.$StackEntityTable(this);
|
||||
late final i5.$LocalAssetEntityTable localAssetEntity = i5
|
||||
.$LocalAssetEntityTable(this);
|
||||
late final i5.$RemoteAlbumEntityTable remoteAlbumEntity = i5
|
||||
late final i6.$RemoteAlbumEntityTable remoteAlbumEntity = i6
|
||||
.$RemoteAlbumEntityTable(this);
|
||||
late final i6.$LocalAlbumEntityTable localAlbumEntity = i6
|
||||
late final i7.$LocalAlbumEntityTable localAlbumEntity = i7
|
||||
.$LocalAlbumEntityTable(this);
|
||||
late final i7.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i7
|
||||
late final i8.$LocalAlbumAssetEntityTable localAlbumAssetEntity = i8
|
||||
.$LocalAlbumAssetEntityTable(this);
|
||||
late final i8.$AuthUserEntityTable authUserEntity = i8.$AuthUserEntityTable(
|
||||
late final i9.$AuthUserEntityTable authUserEntity = i9.$AuthUserEntityTable(
|
||||
this,
|
||||
);
|
||||
late final i9.$UserMetadataEntityTable userMetadataEntity = i9
|
||||
late final i10.$UserMetadataEntityTable userMetadataEntity = i10
|
||||
.$UserMetadataEntityTable(this);
|
||||
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable(
|
||||
late final i11.$PartnerEntityTable partnerEntity = i11.$PartnerEntityTable(
|
||||
this,
|
||||
);
|
||||
late final i11.$RemoteExifEntityTable remoteExifEntity = i11
|
||||
late final i12.$RemoteExifEntityTable remoteExifEntity = i12
|
||||
.$RemoteExifEntityTable(this);
|
||||
late final i12.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i12
|
||||
late final i13.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = i13
|
||||
.$RemoteAlbumAssetEntityTable(this);
|
||||
late final i13.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i13
|
||||
late final i14.$RemoteAlbumUserEntityTable remoteAlbumUserEntity = i14
|
||||
.$RemoteAlbumUserEntityTable(this);
|
||||
late final i14.$MemoryEntityTable memoryEntity = i14.$MemoryEntityTable(this);
|
||||
late final i15.$MemoryAssetEntityTable memoryAssetEntity = i15
|
||||
late final i15.$MemoryEntityTable memoryEntity = i15.$MemoryEntityTable(this);
|
||||
late final i16.$MemoryAssetEntityTable memoryAssetEntity = i16
|
||||
.$MemoryAssetEntityTable(this);
|
||||
late final i16.$PersonEntityTable personEntity = i16.$PersonEntityTable(this);
|
||||
late final i17.$AssetFaceEntityTable assetFaceEntity = i17
|
||||
late final i17.$PersonEntityTable personEntity = i17.$PersonEntityTable(this);
|
||||
late final i18.$AssetFaceEntityTable assetFaceEntity = i18
|
||||
.$AssetFaceEntityTable(this);
|
||||
late final i18.$StoreEntityTable storeEntity = i18.$StoreEntityTable(this);
|
||||
late final i19.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i19
|
||||
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
|
||||
late final i20.$TrashedLocalAssetEntityTable trashedLocalAssetEntity = i20
|
||||
.$TrashedLocalAssetEntityTable(this);
|
||||
i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer(
|
||||
i21.MergedAssetDrift get mergedAssetDrift => i22.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i20.MergedAssetDrift>(i20.MergedAssetDrift.new);
|
||||
).accessor<i21.MergedAssetDrift>(i21.MergedAssetDrift.new);
|
||||
i1.UploadTasksDrift get uploadTasksDrift => i22.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i1.UploadTasksDrift>(i1.UploadTasksDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@override
|
||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
uploadTasks,
|
||||
uploadTaskStats,
|
||||
i1.updateStatsInsert,
|
||||
i1.updateStatsUpdate,
|
||||
i1.updateStatsDelete,
|
||||
i1.idxUploadTasksLocalId,
|
||||
i1.idxUploadTasksAssetData,
|
||||
i1.$drift0,
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
@@ -96,11 +111,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
i4.idxLocalAssetChecksum,
|
||||
i2.idxRemoteAssetOwnerChecksum,
|
||||
i2.uQRemoteAssetsOwnerChecksum,
|
||||
i2.uQRemoteAssetsOwnerLibraryChecksum,
|
||||
i2.idxRemoteAssetChecksum,
|
||||
i5.idxLocalAssetChecksum,
|
||||
i3.idxRemoteAssetOwnerChecksum,
|
||||
i3.uQRemoteAssetsOwnerChecksum,
|
||||
i3.uQRemoteAssetsOwnerLibraryChecksum,
|
||||
i3.idxRemoteAssetChecksum,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
@@ -113,13 +128,34 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
i11.idxLatLng,
|
||||
i19.idxTrashedLocalAssetChecksum,
|
||||
i19.idxTrashedLocalAssetAlbum,
|
||||
i12.idxLatLng,
|
||||
i20.idxTrashedLocalAssetChecksum,
|
||||
i20.idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
get streamUpdateRules => const i0.StreamQueryUpdateRules([
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'upload_tasks',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('upload_task_stats', kind: i0.UpdateKind.update)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'upload_tasks',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [i0.TableUpdate('upload_task_stats', kind: i0.UpdateKind.update)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'upload_tasks',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('upload_task_stats', kind: i0.UpdateKind.update)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'user_entity',
|
||||
@@ -304,47 +340,51 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
||||
class $DriftManager {
|
||||
final $Drift _db;
|
||||
$DriftManager(this._db);
|
||||
i1.$$UserEntityTableTableManager get userEntity =>
|
||||
i1.$$UserEntityTableTableManager(_db, _db.userEntity);
|
||||
i2.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
||||
i2.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||
i3.$$StackEntityTableTableManager get stackEntity =>
|
||||
i3.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
i4.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i4.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i5.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i5.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i6.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i6.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i7.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i7
|
||||
i1.$UploadTasksTableManager get uploadTasks =>
|
||||
i1.$UploadTasksTableManager(_db, _db.uploadTasks);
|
||||
i1.$UploadTaskStatsTableManager get uploadTaskStats =>
|
||||
i1.$UploadTaskStatsTableManager(_db, _db.uploadTaskStats);
|
||||
i2.$$UserEntityTableTableManager get userEntity =>
|
||||
i2.$$UserEntityTableTableManager(_db, _db.userEntity);
|
||||
i3.$$RemoteAssetEntityTableTableManager get remoteAssetEntity =>
|
||||
i3.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity);
|
||||
i4.$$StackEntityTableTableManager get stackEntity =>
|
||||
i4.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||
i5.$$LocalAssetEntityTableTableManager get localAssetEntity =>
|
||||
i5.$$LocalAssetEntityTableTableManager(_db, _db.localAssetEntity);
|
||||
i6.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity =>
|
||||
i6.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity);
|
||||
i7.$$LocalAlbumEntityTableTableManager get localAlbumEntity =>
|
||||
i7.$$LocalAlbumEntityTableTableManager(_db, _db.localAlbumEntity);
|
||||
i8.$$LocalAlbumAssetEntityTableTableManager get localAlbumAssetEntity => i8
|
||||
.$$LocalAlbumAssetEntityTableTableManager(_db, _db.localAlbumAssetEntity);
|
||||
i8.$$AuthUserEntityTableTableManager get authUserEntity =>
|
||||
i8.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
|
||||
i9.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i9.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i10.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i10.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i11.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i11.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i12.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i12.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
i9.$$AuthUserEntityTableTableManager get authUserEntity =>
|
||||
i9.$$AuthUserEntityTableTableManager(_db, _db.authUserEntity);
|
||||
i10.$$UserMetadataEntityTableTableManager get userMetadataEntity =>
|
||||
i10.$$UserMetadataEntityTableTableManager(_db, _db.userMetadataEntity);
|
||||
i11.$$PartnerEntityTableTableManager get partnerEntity =>
|
||||
i11.$$PartnerEntityTableTableManager(_db, _db.partnerEntity);
|
||||
i12.$$RemoteExifEntityTableTableManager get remoteExifEntity =>
|
||||
i12.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity);
|
||||
i13.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity =>
|
||||
i13.$$RemoteAlbumAssetEntityTableTableManager(
|
||||
_db,
|
||||
_db.remoteAlbumAssetEntity,
|
||||
);
|
||||
i13.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i13
|
||||
i14.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i14
|
||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||
i14.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i14.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i15.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i15.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i16.$$PersonEntityTableTableManager get personEntity =>
|
||||
i16.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i17.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i17.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i18.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i18.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i19.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
|
||||
i19.$$TrashedLocalAssetEntityTableTableManager(
|
||||
i15.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||
i15.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||
i16.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||
i16.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||
i17.$$PersonEntityTableTableManager get personEntity =>
|
||||
i17.$$PersonEntityTableTableManager(_db, _db.personEntity);
|
||||
i18.$$AssetFaceEntityTableTableManager get assetFaceEntity =>
|
||||
i18.$$AssetFaceEntityTableTableManager(_db, _db.assetFaceEntity);
|
||||
i19.$$StoreEntityTableTableManager get storeEntity =>
|
||||
i19.$$StoreEntityTableTableManager(_db, _db.storeEntity);
|
||||
i20.$$TrashedLocalAssetEntityTableTableManager get trashedLocalAssetEntity =>
|
||||
i20.$$TrashedLocalAssetEntityTableTableManager(
|
||||
_db,
|
||||
_db.trashedLocalAssetEntity,
|
||||
);
|
||||
|
||||
@@ -5485,6 +5485,701 @@ i1.GeneratedColumn<String> _column_95(String aliasedName) =>
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
);
|
||||
|
||||
final class Schema14 extends i0.VersionedSchema {
|
||||
Schema14({required super.database}) : super(version: 14);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
uploadTasks,
|
||||
uploadTaskStats,
|
||||
updateStatsInsert,
|
||||
updateStatsUpdate,
|
||||
updateStatsDelete,
|
||||
idxUploadTasksLocalId,
|
||||
idxUploadTasksAssetData,
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAssetChecksum,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
idxLatLng,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
late final Shape24 uploadTasks = Shape24(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'upload_tasks',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_96,
|
||||
_column_97,
|
||||
_column_98,
|
||||
_column_99,
|
||||
_column_100,
|
||||
_column_101,
|
||||
_column_102,
|
||||
_column_103,
|
||||
_column_104,
|
||||
_column_105,
|
||||
_column_106,
|
||||
_column_107,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape25 uploadTaskStats = Shape25(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'upload_task_stats',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [
|
||||
_column_108,
|
||||
_column_109,
|
||||
_column_110,
|
||||
_column_111,
|
||||
_column_112,
|
||||
_column_113,
|
||||
_column_114,
|
||||
_column_115,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Trigger updateStatsInsert = i1.Trigger(
|
||||
'CREATE TRIGGER update_stats_insert BEFORE INSERT ON upload_tasks BEGIN UPDATE upload_task_stats SET pending_downloads = pending_downloads +(NEW.status = 0), queued_downloads = queued_downloads +(NEW.status = 1), failed_downloads = failed_downloads +(NEW.status = 2), pending_uploads = pending_uploads +(NEW.status = 3), queued_uploads = queued_uploads +(NEW.status = 4), failed_uploads = failed_uploads +(NEW.status = 5), completed_uploads = completed_uploads +(NEW.status = 6), skipped_uploads = skipped_uploads +(NEW.status = 7);END',
|
||||
'update_stats_insert',
|
||||
);
|
||||
final i1.Trigger updateStatsUpdate = i1.Trigger(
|
||||
'CREATE TRIGGER update_stats_update BEFORE UPDATE OF status ON upload_tasks WHEN OLD.status != NEW.status BEGIN UPDATE upload_task_stats SET pending_downloads = pending_downloads -(OLD.status = 0)+(NEW.status = 0), queued_downloads = queued_downloads -(OLD.status = 1)+(NEW.status = 1), failed_downloads = failed_downloads -(OLD.status = 2)+(NEW.status = 2), pending_uploads = pending_uploads -(OLD.status = 3)+(NEW.status = 3), queued_uploads = queued_uploads -(OLD.status = 4)+(NEW.status = 4), failed_uploads = failed_uploads -(OLD.status = 5)+(NEW.status = 5), completed_uploads = completed_uploads -(OLD.status = 6)+(NEW.status = 6), skipped_uploads = skipped_uploads -(OLD.status = 7)+(NEW.status = 7);END',
|
||||
'update_stats_update',
|
||||
);
|
||||
final i1.Trigger updateStatsDelete = i1.Trigger(
|
||||
'CREATE TRIGGER update_stats_delete BEFORE DELETE ON upload_tasks BEGIN UPDATE upload_task_stats SET pending_downloads = pending_downloads -(OLD.status = 0), queued_downloads = queued_downloads -(OLD.status = 1), failed_downloads = failed_downloads -(OLD.status = 2), pending_uploads = pending_uploads -(OLD.status = 3), queued_uploads = queued_uploads -(OLD.status = 4), failed_uploads = failed_uploads -(OLD.status = 5), completed_uploads = completed_uploads -(OLD.status = 6), skipped_uploads = skipped_uploads -(OLD.status = 7);END',
|
||||
'update_stats_delete',
|
||||
);
|
||||
final i1.Index idxUploadTasksLocalId = i1.Index(
|
||||
'idx_upload_tasks_local_id',
|
||||
'CREATE UNIQUE INDEX idx_upload_tasks_local_id ON upload_tasks (local_id, live_photo_video_id)',
|
||||
);
|
||||
final i1.Index idxUploadTasksAssetData = i1.Index(
|
||||
'idx_upload_tasks_asset_data',
|
||||
'CREATE INDEX idx_upload_tasks_asset_data ON upload_tasks (status, priority DESC, created_at)',
|
||||
);
|
||||
late final Shape20 userEntity = Shape20(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_91,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape17 remoteAssetEntity = Shape17(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_13,
|
||||
_column_14,
|
||||
_column_15,
|
||||
_column_16,
|
||||
_column_17,
|
||||
_column_18,
|
||||
_column_19,
|
||||
_column_20,
|
||||
_column_21,
|
||||
_column_86,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 stackEntity = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'stack_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape2 localAssetEntity = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape9 remoteAlbumEntity = Shape9(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_56,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_57,
|
||||
_column_58,
|
||||
_column_59,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape19 localAlbumEntity = Shape19(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_5,
|
||||
_column_31,
|
||||
_column_32,
|
||||
_column_90,
|
||||
_column_33,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape22 localAlbumAssetEntity = Shape22(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'local_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_34, _column_35, _column_33],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLocalAssetChecksum = i1.Index(
|
||||
'idx_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
|
||||
'idx_remote_asset_owner_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
|
||||
);
|
||||
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
|
||||
'UQ_remote_assets_owner_library_checksum',
|
||||
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetChecksum = i1.Index(
|
||||
'idx_remote_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
|
||||
);
|
||||
late final Shape21 authUserEntity = Shape21(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'auth_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_1,
|
||||
_column_3,
|
||||
_column_2,
|
||||
_column_84,
|
||||
_column_85,
|
||||
_column_92,
|
||||
_column_93,
|
||||
_column_7,
|
||||
_column_94,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 userMetadataEntity = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'user_metadata_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
|
||||
columns: [_column_25, _column_26, _column_27],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape5 partnerEntity = Shape5(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'partner_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
|
||||
columns: [_column_28, _column_29, _column_30],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape8 remoteExifEntity = Shape8(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_exif_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_37,
|
||||
_column_38,
|
||||
_column_39,
|
||||
_column_40,
|
||||
_column_41,
|
||||
_column_11,
|
||||
_column_10,
|
||||
_column_42,
|
||||
_column_43,
|
||||
_column_44,
|
||||
_column_45,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_48,
|
||||
_column_49,
|
||||
_column_50,
|
||||
_column_51,
|
||||
_column_52,
|
||||
_column_53,
|
||||
_column_54,
|
||||
_column_55,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape7 remoteAlbumAssetEntity = Shape7(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
|
||||
columns: [_column_36, _column_60],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape10 remoteAlbumUserEntity = Shape10(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_album_user_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
|
||||
columns: [_column_60, _column_25, _column_61],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape11 memoryEntity = Shape11(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_18,
|
||||
_column_15,
|
||||
_column_8,
|
||||
_column_62,
|
||||
_column_63,
|
||||
_column_64,
|
||||
_column_65,
|
||||
_column_66,
|
||||
_column_67,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape12 memoryAssetEntity = Shape12(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'memory_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
|
||||
columns: [_column_36, _column_68],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape14 personEntity = Shape14(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'person_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_15,
|
||||
_column_1,
|
||||
_column_69,
|
||||
_column_71,
|
||||
_column_72,
|
||||
_column_73,
|
||||
_column_74,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape15 assetFaceEntity = Shape15(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'asset_face_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [
|
||||
_column_0,
|
||||
_column_36,
|
||||
_column_76,
|
||||
_column_77,
|
||||
_column_78,
|
||||
_column_79,
|
||||
_column_80,
|
||||
_column_81,
|
||||
_column_82,
|
||||
_column_83,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape18 storeEntity = Shape18(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'store_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id)'],
|
||||
columns: [_column_87, _column_88, _column_89],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape23 trashedLocalAssetEntity = Shape23(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'trashed_local_asset_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(id, album_id)'],
|
||||
columns: [
|
||||
_column_1,
|
||||
_column_8,
|
||||
_column_9,
|
||||
_column_5,
|
||||
_column_10,
|
||||
_column_11,
|
||||
_column_12,
|
||||
_column_0,
|
||||
_column_95,
|
||||
_column_22,
|
||||
_column_14,
|
||||
_column_23,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxLatLng = i1.Index(
|
||||
'idx_lat_lng',
|
||||
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
|
||||
'idx_trashed_local_asset_checksum',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
|
||||
);
|
||||
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
|
||||
'idx_trashed_local_asset_album',
|
||||
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape24 extends i0.VersionedTable {
|
||||
Shape24({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get attempts =>
|
||||
columnsByName['attempts']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get filePath =>
|
||||
columnsByName['file_path']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get isLivePhoto =>
|
||||
columnsByName['is_live_photo']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get lastError =>
|
||||
columnsByName['last_error']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get livePhotoVideoId =>
|
||||
columnsByName['live_photo_video_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get localId =>
|
||||
columnsByName['local_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get method =>
|
||||
columnsByName['method']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<double> get priority =>
|
||||
columnsByName['priority']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<int> get retryAfter =>
|
||||
columnsByName['retry_after']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get status =>
|
||||
columnsByName['status']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_96(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'id',
|
||||
aliasedName,
|
||||
true,
|
||||
hasAutoIncrement: true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'PRIMARY KEY AUTOINCREMENT',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_97(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'attempts',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_98(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'created_at',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_99(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'file_path',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_100(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'is_live_photo',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_101(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'last_error',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_102(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'live_photo_video_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_103(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'local_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_104(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'method',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<double> _column_105(String aliasedName) =>
|
||||
i1.GeneratedColumn<double>(
|
||||
'priority',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.double,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_106(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'retry_after',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_107(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'status',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
|
||||
class Shape25 extends i0.VersionedTable {
|
||||
Shape25({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get pendingDownloads =>
|
||||
columnsByName['pending_downloads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get pendingUploads =>
|
||||
columnsByName['pending_uploads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get queuedDownloads =>
|
||||
columnsByName['queued_downloads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get queuedUploads =>
|
||||
columnsByName['queued_uploads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get failedDownloads =>
|
||||
columnsByName['failed_downloads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get failedUploads =>
|
||||
columnsByName['failed_uploads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get completedUploads =>
|
||||
columnsByName['completed_uploads']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get skippedUploads =>
|
||||
columnsByName['skipped_uploads']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_108(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'pending_downloads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_109(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'pending_uploads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_110(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'queued_downloads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_111(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'queued_uploads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_112(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'failed_downloads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_113(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'failed_uploads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_114(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'completed_uploads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_115(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'skipped_uploads',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -5498,6 +6193,7 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
||||
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -5561,6 +6257,11 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from12To13(migrator, schema);
|
||||
return 13;
|
||||
case 13:
|
||||
final schema = Schema14(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from13To14(migrator, schema);
|
||||
return 14;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -5580,6 +6281,7 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
|
||||
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
|
||||
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
|
||||
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -5594,5 +6296,6 @@ i1.OnUpgrade stepByStep({
|
||||
from10To11: from10To11,
|
||||
from11To12: from11To12,
|
||||
from12To13: from12To13,
|
||||
from13To14: from13To14,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
@@ -90,17 +89,5 @@ class StorageRepository {
|
||||
} catch (error, stackTrace) {
|
||||
log.warning("Error clearing cache", error, stackTrace);
|
||||
}
|
||||
|
||||
if (!CurrentPlatform.isIOS) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (await Directory.systemTemp.exists()) {
|
||||
await Directory.systemTemp.delete(recursive: true);
|
||||
}
|
||||
} catch (error, stackTrace) {
|
||||
log.warning("Error deleting temporary directory", error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-17
@@ -3,7 +3,6 @@ import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -11,7 +10,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/domain/services/background_worker.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
@@ -55,6 +53,9 @@ void main() async {
|
||||
// Warm-up isolate pool for worker manager
|
||||
await workerManagerPatch.init(dynamicSpawning: true, isolatesCount: max(Platform.numberOfProcessors - 1, 5));
|
||||
await migrateDatabaseIfNeeded(isar, drift);
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
await uploadApi.initialize();
|
||||
}
|
||||
HttpSSLOptions.apply();
|
||||
|
||||
runApp(
|
||||
@@ -102,18 +103,6 @@ Future<void> initApp() async {
|
||||
|
||||
initializeTimeZones();
|
||||
|
||||
// Initialize the file downloader
|
||||
await FileDownloader().configure(
|
||||
// maxConcurrent: 6, maxConcurrentByHost(server):6, maxConcurrentByGroup: 3
|
||||
|
||||
// On Android, if files are larger than 256MB, run in foreground service
|
||||
globalConfig: [(Config.holdingQueue, (6, 6, 3)), (Config.runInForegroundIfFileLargerThan, 256)],
|
||||
);
|
||||
|
||||
await FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false);
|
||||
|
||||
await FileDownloader().trackTasks();
|
||||
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
for (final license in nonPubLicenses.entries) {
|
||||
yield LicenseEntryWithLineBreaks([license.key], license.value);
|
||||
@@ -199,9 +188,6 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
Intl.defaultLocale = context.locale.toLanguageTag();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
configureFileDownloaderNotifications();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -7,7 +7,7 @@ import 'package:path/path.dart';
|
||||
|
||||
enum ShareIntentAttachmentType { image, video }
|
||||
|
||||
enum UploadStatus { enqueued, running, complete, notFound, failed, canceled, waitingToRetry, paused }
|
||||
enum UploadStatus { enqueued, running, complete, notFound, failed, canceled, waitingToRetry, paused, preparing }
|
||||
|
||||
class ShareIntentAttachment {
|
||||
final String path;
|
||||
|
||||
@@ -93,7 +93,7 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
|
||||
Logger("DriftBackupPage").warning("Remote sync did not complete successfully, skipping backup");
|
||||
return;
|
||||
}
|
||||
await backupNotifier.startBackup(currentUser.id);
|
||||
await backupNotifier.startBackup();
|
||||
}
|
||||
|
||||
Future<void> stopBackup() async {
|
||||
|
||||
@@ -116,11 +116,10 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
||||
unawaited(
|
||||
backupNotifier.cancel().whenComplete(
|
||||
() => backgroundSync.syncRemote().then((success) {
|
||||
if (success) {
|
||||
return backupNotifier.startBackup(user.id);
|
||||
} else {
|
||||
Logger('DriftBackupAlbumSelectionPage').warning('Background sync failed, not starting backup');
|
||||
if (!success) {
|
||||
Logger('DriftBackupAlbumSelectionPage').warning('Remote sync failed');
|
||||
}
|
||||
return backupNotifier.startBackup();
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ class DriftBackupOptionsPage extends ConsumerWidget {
|
||||
backupNotifier.cancel().whenComplete(
|
||||
() => backgroundSync.syncRemote().then((success) {
|
||||
if (success) {
|
||||
return backupNotifier.startBackup(currentUser.id);
|
||||
return backupNotifier.startBackup();
|
||||
} else {
|
||||
Logger('DriftBackupOptionsPage').warning('Background sync failed, not starting backup');
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -53,7 +53,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
final infoProvider = ref.read(serverInfoProvider.notifier);
|
||||
final wsProvider = ref.read(websocketProvider.notifier);
|
||||
final backgroundManager = ref.read(backgroundSyncProvider);
|
||||
final backupProvider = ref.read(driftBackupProvider.notifier);
|
||||
|
||||
unawaited(
|
||||
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
|
||||
@@ -63,22 +62,13 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
unawaited(infoProvider.getServerInfo());
|
||||
|
||||
if (Store.isBetaTimelineEnabled) {
|
||||
bool syncSuccess = false;
|
||||
await Future.wait([
|
||||
backgroundManager.syncLocal(full: true),
|
||||
backgroundManager.syncRemote().then((success) => syncSuccess = success),
|
||||
backgroundManager.syncRemote(),
|
||||
]);
|
||||
|
||||
if (syncSuccess) {
|
||||
await Future.wait([
|
||||
backgroundManager.hashAssets().then((_) {
|
||||
_resumeBackup(backupProvider);
|
||||
}),
|
||||
_resumeBackup(backupProvider),
|
||||
]);
|
||||
} else {
|
||||
await backgroundManager.hashAssets();
|
||||
}
|
||||
await backgroundManager.hashAssets();
|
||||
await uploadApi.refresh();
|
||||
|
||||
if (Store.get(StoreKey.syncAlbums, false)) {
|
||||
await backgroundManager.syncLinkedAlbum();
|
||||
@@ -126,17 +116,6 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup(DriftBackupNotifier notifier) async {
|
||||
final isEnableBackup = Store.get(StoreKey.enableBackup, false);
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
if (currentUser != null) {
|
||||
unawaited(notifier.handleBackupResume(currentUser.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
|
||||
@@ -37,10 +37,7 @@ class ShareIntentPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void upload() async {
|
||||
for (final attachment in candidates) {
|
||||
await ref.read(shareIntentUploadProvider.notifier).upload(attachment.file);
|
||||
}
|
||||
|
||||
await ref.read(shareIntentUploadProvider.notifier).upload(candidates);
|
||||
isUploaded.value = true;
|
||||
}
|
||||
|
||||
@@ -212,6 +209,11 @@ class UploadStatusIcon extends StatelessWidget {
|
||||
color: context.primaryColor,
|
||||
semanticLabel: 'paused'.tr(),
|
||||
),
|
||||
UploadStatus.preparing => Icon(
|
||||
Icons.hourglass_top_rounded,
|
||||
color: context.primaryColor,
|
||||
semanticLabel: 'preparing'.tr(),
|
||||
),
|
||||
};
|
||||
|
||||
return statusIcon;
|
||||
|
||||
Generated
+368
@@ -0,0 +1,368 @@
|
||||
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
|
||||
// See also: https://pub.dev/packages/pigeon
|
||||
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
|
||||
|
||||
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
PlatformException _createConnectionError(String channelName) {
|
||||
return PlatformException(
|
||||
code: 'channel-error',
|
||||
message: 'Unable to establish connection on channel: "$channelName".',
|
||||
);
|
||||
}
|
||||
|
||||
bool _deepEquals(Object? a, Object? b) {
|
||||
if (a is List && b is List) {
|
||||
return a.length == b.length && a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
|
||||
}
|
||||
if (a is Map && b is Map) {
|
||||
return a.length == b.length &&
|
||||
a.entries.every(
|
||||
(MapEntry<Object?, Object?> entry) =>
|
||||
(b as Map<Object?, Object?>).containsKey(entry.key) && _deepEquals(entry.value, b[entry.key]),
|
||||
);
|
||||
}
|
||||
return a == b;
|
||||
}
|
||||
|
||||
enum UploadApiErrorCode {
|
||||
unknown,
|
||||
assetNotFound,
|
||||
fileNotFound,
|
||||
resourceNotFound,
|
||||
invalidResource,
|
||||
encodingFailed,
|
||||
writeFailed,
|
||||
notEnoughSpace,
|
||||
networkError,
|
||||
photosInternalError,
|
||||
photosUnknownError,
|
||||
noServerUrl,
|
||||
noDeviceId,
|
||||
noAccessToken,
|
||||
interrupted,
|
||||
cancelled,
|
||||
downloadStalled,
|
||||
forceQuit,
|
||||
outOfResources,
|
||||
backgroundUpdatesDisabled,
|
||||
uploadTimeout,
|
||||
iCloudRateLimit,
|
||||
iCloudThrottled,
|
||||
}
|
||||
|
||||
enum UploadApiStatus {
|
||||
downloadPending,
|
||||
downloadQueued,
|
||||
downloadFailed,
|
||||
uploadPending,
|
||||
uploadQueued,
|
||||
uploadFailed,
|
||||
uploadComplete,
|
||||
uploadSkipped,
|
||||
}
|
||||
|
||||
class UploadApiTaskStatus {
|
||||
UploadApiTaskStatus({
|
||||
required this.id,
|
||||
required this.filename,
|
||||
required this.status,
|
||||
this.errorCode,
|
||||
this.httpStatusCode,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
String filename;
|
||||
|
||||
UploadApiStatus status;
|
||||
|
||||
UploadApiErrorCode? errorCode;
|
||||
|
||||
int? httpStatusCode;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[id, filename, status, errorCode, httpStatusCode];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static UploadApiTaskStatus decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return UploadApiTaskStatus(
|
||||
id: result[0]! as String,
|
||||
filename: result[1]! as String,
|
||||
status: result[2]! as UploadApiStatus,
|
||||
errorCode: result[3] as UploadApiErrorCode?,
|
||||
httpStatusCode: result[4] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! UploadApiTaskStatus || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
}
|
||||
|
||||
class UploadApiTaskProgress {
|
||||
UploadApiTaskProgress({required this.id, required this.progress, this.speed, this.totalBytes});
|
||||
|
||||
String id;
|
||||
|
||||
double progress;
|
||||
|
||||
double? speed;
|
||||
|
||||
int? totalBytes;
|
||||
|
||||
List<Object?> _toList() {
|
||||
return <Object?>[id, progress, speed, totalBytes];
|
||||
}
|
||||
|
||||
Object encode() {
|
||||
return _toList();
|
||||
}
|
||||
|
||||
static UploadApiTaskProgress decode(Object result) {
|
||||
result as List<Object?>;
|
||||
return UploadApiTaskProgress(
|
||||
id: result[0]! as String,
|
||||
progress: result[1]! as double,
|
||||
speed: result[2] as double?,
|
||||
totalBytes: result[3] as int?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
bool operator ==(Object other) {
|
||||
if (other is! UploadApiTaskProgress || other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return _deepEquals(encode(), other.encode());
|
||||
}
|
||||
|
||||
@override
|
||||
// ignore: avoid_equals_and_hash_code_on_mutable_classes
|
||||
int get hashCode => Object.hashAll(_toList());
|
||||
}
|
||||
|
||||
class _PigeonCodec extends StandardMessageCodec {
|
||||
const _PigeonCodec();
|
||||
@override
|
||||
void writeValue(WriteBuffer buffer, Object? value) {
|
||||
if (value is int) {
|
||||
buffer.putUint8(4);
|
||||
buffer.putInt64(value);
|
||||
} else if (value is UploadApiErrorCode) {
|
||||
buffer.putUint8(129);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is UploadApiStatus) {
|
||||
buffer.putUint8(130);
|
||||
writeValue(buffer, value.index);
|
||||
} else if (value is UploadApiTaskStatus) {
|
||||
buffer.putUint8(131);
|
||||
writeValue(buffer, value.encode());
|
||||
} else if (value is UploadApiTaskProgress) {
|
||||
buffer.putUint8(132);
|
||||
writeValue(buffer, value.encode());
|
||||
} else {
|
||||
super.writeValue(buffer, value);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Object? readValueOfType(int type, ReadBuffer buffer) {
|
||||
switch (type) {
|
||||
case 129:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : UploadApiErrorCode.values[value];
|
||||
case 130:
|
||||
final int? value = readValue(buffer) as int?;
|
||||
return value == null ? null : UploadApiStatus.values[value];
|
||||
case 131:
|
||||
return UploadApiTaskStatus.decode(readValue(buffer)!);
|
||||
case 132:
|
||||
return UploadApiTaskProgress.decode(readValue(buffer)!);
|
||||
default:
|
||||
return super.readValueOfType(type, buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const StandardMethodCodec pigeonMethodCodec = StandardMethodCodec(_PigeonCodec());
|
||||
|
||||
class UploadApi {
|
||||
/// Constructor for [UploadApi]. The [binaryMessenger] named argument is
|
||||
/// available for dependency injection. If it is left null, the default
|
||||
/// BinaryMessenger will be used which routes to the host platform.
|
||||
UploadApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
|
||||
: pigeonVar_binaryMessenger = binaryMessenger,
|
||||
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
|
||||
final BinaryMessenger? pigeonVar_binaryMessenger;
|
||||
|
||||
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
|
||||
|
||||
final String pigeonVar_messageChannelSuffix;
|
||||
|
||||
Future<void> initialize() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.UploadApi.initialize$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.UploadApi.refresh$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelAll() async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.UploadApi.cancelAll$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enqueueAssets(List<String> localIds) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.UploadApi.enqueueAssets$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[localIds]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> enqueueFiles(List<String> paths) async {
|
||||
final String pigeonVar_channelName =
|
||||
'dev.flutter.pigeon.immich_mobile.UploadApi.enqueueFiles$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[paths]);
|
||||
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||
if (pigeonVar_replyList == null) {
|
||||
throw _createConnectionError(pigeonVar_channelName);
|
||||
} else if (pigeonVar_replyList.length > 1) {
|
||||
throw PlatformException(
|
||||
code: pigeonVar_replyList[0]! as String,
|
||||
message: pigeonVar_replyList[1] as String?,
|
||||
details: pigeonVar_replyList[2],
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Stream<UploadApiTaskStatus> streamStatus({String instanceName = ''}) {
|
||||
if (instanceName.isNotEmpty) {
|
||||
instanceName = '.$instanceName';
|
||||
}
|
||||
final EventChannel streamStatusChannel = EventChannel(
|
||||
'dev.flutter.pigeon.immich_mobile.UploadFlutterApi.streamStatus$instanceName',
|
||||
pigeonMethodCodec,
|
||||
);
|
||||
return streamStatusChannel.receiveBroadcastStream().map((dynamic event) {
|
||||
return event as UploadApiTaskStatus;
|
||||
});
|
||||
}
|
||||
|
||||
Stream<UploadApiTaskProgress> streamProgress({String instanceName = ''}) {
|
||||
if (instanceName.isNotEmpty) {
|
||||
instanceName = '.$instanceName';
|
||||
}
|
||||
final EventChannel streamProgressChannel = EventChannel(
|
||||
'dev.flutter.pigeon.immich_mobile.UploadFlutterApi.streamProgress$instanceName',
|
||||
pigeonMethodCodec,
|
||||
);
|
||||
return streamProgressChannel.receiveBroadcastStream().map((dynamic event) {
|
||||
return event as UploadApiTaskProgress;
|
||||
});
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
@@ -11,7 +10,6 @@ import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
@@ -148,21 +146,13 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
final isAlbumLinkedSyncEnable = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.syncAlbums);
|
||||
|
||||
try {
|
||||
bool syncSuccess = false;
|
||||
await Future.wait([
|
||||
_safeRun(backgroundManager.syncLocal(), "syncLocal"),
|
||||
_safeRun(backgroundManager.syncRemote().then((success) => syncSuccess = success), "syncRemote"),
|
||||
_safeRun(backgroundManager.syncRemote(), "syncRemote"),
|
||||
]);
|
||||
if (syncSuccess) {
|
||||
await Future.wait([
|
||||
_safeRun(backgroundManager.hashAssets(), "hashAssets").then((_) {
|
||||
_resumeBackup();
|
||||
}),
|
||||
_resumeBackup(),
|
||||
]);
|
||||
} else {
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
}
|
||||
|
||||
await _safeRun(backgroundManager.hashAssets(), "hashAssets");
|
||||
await _safeRun(uploadApi.refresh(), "refresh");
|
||||
|
||||
if (isAlbumLinkedSyncEnable) {
|
||||
await _safeRun(backgroundManager.syncLinkedAlbum(), "syncLinkedAlbum");
|
||||
@@ -172,20 +162,6 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resumeBackup() async {
|
||||
final isEnableBackup = _ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
|
||||
|
||||
if (isEnableBackup) {
|
||||
final currentUser = Store.tryGet(StoreKey.currentUser);
|
||||
if (currentUser != null) {
|
||||
await _safeRun(
|
||||
_ref.read(driftBackupProvider.notifier).handleBackupResume(currentUser.id),
|
||||
"handleBackupResume",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to check if operations should continue
|
||||
bool _shouldContinueOperation() {
|
||||
return [AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state) &&
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import 'dart:io';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/models/upload/share_intent_attachment.model.dart';
|
||||
import 'package:immich_mobile/platform/upload_api.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/share_intent_service.dart';
|
||||
import 'package:immich_mobile/services/upload.service.dart';
|
||||
import 'package:path/path.dart';
|
||||
|
||||
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
||||
((ref) => ShareIntentUploadStateNotifier(
|
||||
@@ -25,10 +22,12 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
final AppRouter router;
|
||||
final UploadService _uploadService;
|
||||
final ShareIntentService _shareIntentService;
|
||||
late final StreamSubscription<UploadApiTaskStatus> _taskStatusStream;
|
||||
late final StreamSubscription<UploadApiTaskProgress> _taskProgressStream;
|
||||
|
||||
ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) {
|
||||
_uploadService.taskStatusStream.listen(_updateUploadStatus);
|
||||
_uploadService.taskProgressStream.listen(_taskProgressCallback);
|
||||
_taskStatusStream = _uploadService.taskStatusStream.listen(_updateUploadStatus);
|
||||
_taskProgressStream = _uploadService.taskProgressStream.listen(_taskProgressCallback);
|
||||
}
|
||||
|
||||
void init() {
|
||||
@@ -36,6 +35,13 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
_shareIntentService.init();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_taskStatusStream.cancel());
|
||||
unawaited(_taskProgressStream.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void onSharedMedia(List<ShareIntentAttachment> attachments) {
|
||||
router.removeWhere((route) => route.name == "ShareIntentRoute");
|
||||
clearAttachments();
|
||||
@@ -65,82 +71,35 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
||||
state = [];
|
||||
}
|
||||
|
||||
void _updateUploadStatus(TaskStatusUpdate task) async {
|
||||
if (task.status == TaskStatus.canceled) {
|
||||
return;
|
||||
}
|
||||
|
||||
final taskId = task.task.taskId;
|
||||
void _updateUploadStatus(UploadApiTaskStatus task) {
|
||||
final uploadStatus = switch (task.status) {
|
||||
TaskStatus.complete => UploadStatus.complete,
|
||||
TaskStatus.failed => UploadStatus.failed,
|
||||
TaskStatus.canceled => UploadStatus.canceled,
|
||||
TaskStatus.enqueued => UploadStatus.enqueued,
|
||||
TaskStatus.running => UploadStatus.running,
|
||||
TaskStatus.paused => UploadStatus.paused,
|
||||
TaskStatus.notFound => UploadStatus.notFound,
|
||||
TaskStatus.waitingToRetry => UploadStatus.waitingToRetry,
|
||||
UploadApiStatus.uploadComplete => UploadStatus.complete,
|
||||
UploadApiStatus.uploadFailed || UploadApiStatus.downloadFailed => UploadStatus.failed,
|
||||
UploadApiStatus.uploadQueued => UploadStatus.enqueued,
|
||||
_ => UploadStatus.preparing,
|
||||
};
|
||||
|
||||
final taskId = task.id.toInt();
|
||||
state = [
|
||||
for (final attachment in state)
|
||||
if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment,
|
||||
if (attachment.id == taskId) attachment.copyWith(status: uploadStatus) else attachment,
|
||||
];
|
||||
}
|
||||
|
||||
void _taskProgressCallback(TaskProgressUpdate update) {
|
||||
void _taskProgressCallback(UploadApiTaskProgress update) {
|
||||
// Ignore if the task is canceled or completed
|
||||
if (update.progress == downloadFailed || update.progress == downloadCompleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final taskId = update.task.taskId;
|
||||
final taskId = update.id.toInt();
|
||||
state = [
|
||||
for (final attachment in state)
|
||||
if (attachment.id == taskId.toInt()) attachment.copyWith(uploadProgress: update.progress) else attachment,
|
||||
if (attachment.id == taskId) attachment.copyWith(uploadProgress: update.progress) else attachment,
|
||||
];
|
||||
}
|
||||
|
||||
Future<void> upload(File file) async {
|
||||
final task = await _buildUploadTask(hash(file.path).toString(), file);
|
||||
|
||||
await _uploadService.enqueueTasks([task]);
|
||||
}
|
||||
|
||||
Future<UploadTask> _buildUploadTask(String id, File file, {Map<String, String>? fields}) async {
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final url = Uri.parse('$serverEndpoint/assets').toString();
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
final deviceId = Store.get(StoreKey.deviceId);
|
||||
|
||||
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
|
||||
final stats = await file.stat();
|
||||
final fileCreatedAt = stats.changed;
|
||||
final fileModifiedAt = stats.modified;
|
||||
|
||||
final fieldsMap = {
|
||||
'filename': filename,
|
||||
'deviceAssetId': id,
|
||||
'deviceId': deviceId,
|
||||
'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(),
|
||||
'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(),
|
||||
'isFavorite': 'false',
|
||||
'duration': '0',
|
||||
if (fields != null) ...fields,
|
||||
};
|
||||
|
||||
return UploadTask(
|
||||
taskId: id,
|
||||
httpRequestMethod: 'POST',
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: filename,
|
||||
fields: fieldsMap,
|
||||
baseDirectory: baseDirectory,
|
||||
directory: directory,
|
||||
fileField: 'assetData',
|
||||
group: kManualUploadGroup,
|
||||
updates: Updates.statusAndProgress,
|
||||
);
|
||||
Future<void> upload(List<ShareIntentAttachment> files) {
|
||||
return uploadApi.enqueueFiles(files.map((e) => e.path).toList(growable: false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/platform/upload_api.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/upload.service.dart';
|
||||
@@ -196,6 +194,9 @@ final driftBackupProvider = StateNotifierProvider<DriftBackupNotifier, DriftBack
|
||||
});
|
||||
|
||||
class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
late final StreamSubscription<UploadApiTaskStatus> _taskStatusStream;
|
||||
late final StreamSubscription<UploadApiTaskProgress> _taskProgressStream;
|
||||
|
||||
DriftBackupNotifier(this._uploadService)
|
||||
: super(
|
||||
const DriftBackupState(
|
||||
@@ -211,17 +212,20 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
error: BackupError.none,
|
||||
),
|
||||
) {
|
||||
{
|
||||
_uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
|
||||
_uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
|
||||
}
|
||||
_taskStatusStream = _uploadService.taskStatusStream.listen(_handleTaskStatusUpdate);
|
||||
_taskProgressStream = _uploadService.taskProgressStream.listen(_handleTaskProgressUpdate);
|
||||
}
|
||||
|
||||
final UploadService _uploadService;
|
||||
StreamSubscription<TaskStatusUpdate>? _statusSubscription;
|
||||
StreamSubscription<TaskProgressUpdate>? _progressSubscription;
|
||||
final _logger = Logger("DriftBackupNotifier");
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_taskStatusStream.cancel());
|
||||
unawaited(_taskProgressStream.cancel());
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Remove upload item from state
|
||||
void _removeUploadItem(String taskId) {
|
||||
if (state.uploadItems.containsKey(taskId)) {
|
||||
@@ -231,16 +235,12 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
|
||||
final taskId = update.task.taskId;
|
||||
void _handleTaskStatusUpdate(UploadApiTaskStatus update) {
|
||||
final taskId = update.id;
|
||||
|
||||
switch (update.status) {
|
||||
case TaskStatus.complete:
|
||||
if (update.task.group == kBackupGroup) {
|
||||
if (update.responseStatusCode == 201) {
|
||||
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
|
||||
}
|
||||
}
|
||||
case UploadApiStatus.uploadComplete:
|
||||
state = state.copyWith(backupCount: state.backupCount + 1, remainderCount: state.remainderCount - 1);
|
||||
|
||||
// Remove the completed task from the upload items
|
||||
if (state.uploadItems.containsKey(taskId)) {
|
||||
@@ -249,40 +249,45 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
});
|
||||
}
|
||||
|
||||
case TaskStatus.failed:
|
||||
// Ignore retry errors to avoid confusing users
|
||||
if (update.exception?.description == 'Delayed or retried enqueue failed') {
|
||||
_removeUploadItem(taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
case UploadApiStatus.uploadFailed:
|
||||
final currentItem = state.uploadItems[taskId];
|
||||
if (currentItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String? error;
|
||||
final exception = update.exception;
|
||||
if (exception != null && exception is TaskHttpException) {
|
||||
final message = tryJsonDecode(exception.description)?['message'] as String?;
|
||||
if (message != null) {
|
||||
final responseCode = exception.httpResponseCode;
|
||||
error = "${exception.exceptionType}, response code $responseCode: $message";
|
||||
}
|
||||
}
|
||||
error ??= update.exception?.toString();
|
||||
// String? error;
|
||||
// final exception = update.exception;
|
||||
// if (exception != null && exception is TaskHttpException) {
|
||||
// final message = tryJsonDecode(exception.description)?['message'] as String?;
|
||||
// if (message != null) {
|
||||
// final responseCode = exception.httpResponseCode;
|
||||
// error = "${exception.exceptionType}, response code $responseCode: $message";
|
||||
// }
|
||||
// }
|
||||
// error ??= update.exception?.toString();
|
||||
|
||||
// state = state.copyWith(
|
||||
// uploadItems: {
|
||||
// ...state.uploadItems,
|
||||
// taskId: currentItem.copyWith(isFailed: true, error: error),
|
||||
// },
|
||||
// );
|
||||
// _logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
|
||||
break;
|
||||
|
||||
case UploadApiStatus.uploadPending:
|
||||
state = state.copyWith(
|
||||
uploadItems: {
|
||||
...state.uploadItems,
|
||||
taskId: currentItem.copyWith(isFailed: true, error: error),
|
||||
taskId: DriftUploadStatus(
|
||||
taskId: update.id.toString(),
|
||||
filename: update.filename,
|
||||
fileSize: 0,
|
||||
networkSpeedAsString: "",
|
||||
progress: 0.0,
|
||||
),
|
||||
},
|
||||
);
|
||||
_logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
|
||||
break;
|
||||
|
||||
case TaskStatus.canceled:
|
||||
_removeUploadItem(update.task.taskId);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -290,42 +295,21 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTaskProgressUpdate(TaskProgressUpdate update) {
|
||||
final taskId = update.task.taskId;
|
||||
final filename = update.task.displayName;
|
||||
void _handleTaskProgressUpdate(UploadApiTaskProgress update) {
|
||||
final taskId = update.id;
|
||||
final progress = update.progress;
|
||||
final currentItem = state.uploadItems[taskId];
|
||||
if (currentItem != null) {
|
||||
if (progress == kUploadStatusCanceled) {
|
||||
_removeUploadItem(update.task.taskId);
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
uploadItems: {
|
||||
...state.uploadItems,
|
||||
taskId: update.hasExpectedFileSize
|
||||
? currentItem.copyWith(
|
||||
progress: progress,
|
||||
fileSize: update.expectedFileSize,
|
||||
networkSpeedAsString: update.networkSpeedAsString,
|
||||
)
|
||||
: currentItem.copyWith(progress: progress),
|
||||
},
|
||||
);
|
||||
|
||||
if (currentItem == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
uploadItems: {
|
||||
...state.uploadItems,
|
||||
taskId: DriftUploadStatus(
|
||||
taskId: taskId,
|
||||
filename: filename,
|
||||
taskId: currentItem.copyWith(
|
||||
progress: progress,
|
||||
fileSize: update.expectedFileSize,
|
||||
networkSpeedAsString: update.networkSpeedAsString,
|
||||
fileSize: update.totalBytes,
|
||||
networkSpeedAsString: update.speed?.toStringAsFixed(2) ?? "",
|
||||
),
|
||||
},
|
||||
);
|
||||
@@ -350,52 +334,18 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
|
||||
state = state.copyWith(isSyncing: isSyncing);
|
||||
}
|
||||
|
||||
Future<void> startBackup(String userId) {
|
||||
Future<void> startBackup() {
|
||||
state = state.copyWith(error: BackupError.none);
|
||||
return _uploadService.startBackup(userId, _updateEnqueueCount);
|
||||
}
|
||||
|
||||
void _updateEnqueueCount(EnqueueStatus status) {
|
||||
state = state.copyWith(enqueueCount: status.enqueueCount, enqueueTotalCount: status.totalCount);
|
||||
return _uploadService.startBackup();
|
||||
}
|
||||
|
||||
Future<void> cancel() async {
|
||||
dPrint(() => "Canceling backup tasks...");
|
||||
state = state.copyWith(enqueueCount: 0, enqueueTotalCount: 0, isCanceling: true, error: BackupError.none);
|
||||
|
||||
final activeTaskCount = await _uploadService.cancelBackup();
|
||||
|
||||
if (activeTaskCount > 0) {
|
||||
dPrint(() => "$activeTaskCount tasks left, continuing to cancel...");
|
||||
await cancel();
|
||||
} else {
|
||||
dPrint(() => "All tasks canceled successfully.");
|
||||
// Clear all upload items when cancellation is complete
|
||||
state = state.copyWith(isCanceling: false, uploadItems: {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleBackupResume(String userId) async {
|
||||
_logger.info("Resuming backup tasks...");
|
||||
state = state.copyWith(error: BackupError.none);
|
||||
final tasks = await _uploadService.getActiveTasks(kBackupGroup);
|
||||
_logger.info("Found ${tasks.length} tasks");
|
||||
|
||||
if (tasks.isEmpty) {
|
||||
// Start a new backup queue
|
||||
_logger.info("Start a new backup queue");
|
||||
return startBackup(userId);
|
||||
}
|
||||
|
||||
_logger.info("Tasks to resume: ${tasks.length}");
|
||||
return _uploadService.resumeBackup();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_statusSubscription?.cancel();
|
||||
_progressSubscription?.cancel();
|
||||
super.dispose();
|
||||
await _uploadService.cancelBackup();
|
||||
dPrint(() => "All tasks canceled successfully.");
|
||||
state = state.copyWith(isCanceling: false, uploadItems: {});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
|
||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||
import 'package:immich_mobile/platform/native_sync_api.g.dart';
|
||||
import 'package:immich_mobile/platform/thumbnail_api.g.dart';
|
||||
import 'package:immich_mobile/platform/upload_api.g.dart';
|
||||
|
||||
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
|
||||
|
||||
@@ -17,3 +18,5 @@ final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
|
||||
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
|
||||
|
||||
final thumbnailApi = ThumbnailApi();
|
||||
|
||||
final uploadApi = UploadApi();
|
||||
|
||||
@@ -1,143 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class UploadTaskWithFile {
|
||||
final File file;
|
||||
final UploadTask task;
|
||||
|
||||
const UploadTaskWithFile({required this.file, required this.task});
|
||||
}
|
||||
|
||||
final uploadRepositoryProvider = Provider((ref) => UploadRepository());
|
||||
|
||||
class UploadRepository {
|
||||
void Function(TaskStatusUpdate)? onUploadStatus;
|
||||
void Function(TaskProgressUpdate)? onTaskProgress;
|
||||
|
||||
UploadRepository() {
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kBackupGroup,
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kBackupLivePhotoGroup,
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
FileDownloader().registerCallbacks(
|
||||
group: kManualUploadGroup,
|
||||
taskStatusCallback: (update) => onUploadStatus?.call(update),
|
||||
taskProgressCallback: (update) => onTaskProgress?.call(update),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> enqueueBackground(UploadTask task) {
|
||||
return FileDownloader().enqueue(task);
|
||||
}
|
||||
|
||||
Future<List<bool>> enqueueBackgroundAll(List<UploadTask> tasks) {
|
||||
return FileDownloader().enqueueAll(tasks);
|
||||
}
|
||||
|
||||
Future<void> deleteDatabaseRecords(String group) {
|
||||
return FileDownloader().database.deleteAllRecords(group: group);
|
||||
}
|
||||
|
||||
Future<bool> cancelAll(String group) {
|
||||
return FileDownloader().cancelAll(group: group);
|
||||
}
|
||||
|
||||
Future<int> reset(String group) {
|
||||
return FileDownloader().reset(group: group);
|
||||
}
|
||||
|
||||
/// Get a list of tasks that are ENQUEUED or RUNNING
|
||||
Future<List<Task>> getActiveTasks(String group) {
|
||||
return FileDownloader().allTasks(group: group);
|
||||
}
|
||||
|
||||
Future<void> start() {
|
||||
return FileDownloader().start();
|
||||
}
|
||||
|
||||
Future<void> getUploadInfo() async {
|
||||
final [enqueuedTasks, runningTasks, canceledTasks, waitingTasks, pausedTasks] = await Future.wait([
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.enqueued, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.running, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.canceled, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.waitingToRetry, group: kBackupGroup),
|
||||
FileDownloader().database.allRecordsWithStatus(TaskStatus.paused, group: kBackupGroup),
|
||||
]);
|
||||
|
||||
dPrint(
|
||||
() =>
|
||||
"""
|
||||
Upload Info:
|
||||
Enqueued: ${enqueuedTasks.length}
|
||||
Running: ${runningTasks.length}
|
||||
Canceled: ${canceledTasks.length}
|
||||
Waiting: ${waitingTasks.length}
|
||||
Paused: ${pausedTasks.length}
|
||||
""",
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> backupWithDartClient(Iterable<UploadTaskWithFile> tasks, CancellationToken cancelToken) async {
|
||||
final httpClient = Client();
|
||||
final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
|
||||
Logger logger = Logger('UploadRepository');
|
||||
for (final candidate in tasks) {
|
||||
if (cancelToken.isCancelled) {
|
||||
logger.warning("Backup was cancelled by the user");
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
final fileStream = candidate.file.openRead();
|
||||
final assetRawUploadData = MultipartFile(
|
||||
"assetData",
|
||||
fileStream,
|
||||
candidate.file.lengthSync(),
|
||||
filename: candidate.task.filename,
|
||||
);
|
||||
|
||||
final baseRequest = MultipartRequest('POST', Uri.parse('$savedEndpoint/assets'));
|
||||
|
||||
baseRequest.headers.addAll(candidate.task.headers);
|
||||
baseRequest.fields.addAll(candidate.task.fields);
|
||||
baseRequest.files.add(assetRawUploadData);
|
||||
|
||||
final response = await httpClient.send(baseRequest, cancellationToken: cancelToken);
|
||||
|
||||
final responseBody = jsonDecode(await response.stream.bytesToString());
|
||||
|
||||
if (![200, 201].contains(response.statusCode)) {
|
||||
final error = responseBody;
|
||||
|
||||
logger.warning(
|
||||
"Error(${error['statusCode']}) uploading ${candidate.task.filename} | Created on ${candidate.task.fields["fileCreatedAt"]} | ${error['error']}",
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
} on CancelledException {
|
||||
logger.warning("Backup was cancelled by the user");
|
||||
break;
|
||||
} catch (error, stackTrace) {
|
||||
logger.warning("Error backup asset: ${error.toString()}: $stackTrace");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,502 +1,43 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:background_downloader/background_downloader.dart';
|
||||
import 'package:cancellation_token_http/http.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/constants.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/platform_extensions.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:immich_mobile/platform/upload_api.g.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
|
||||
|
||||
final uploadServiceProvider = Provider((ref) {
|
||||
final service = UploadService(
|
||||
ref.watch(uploadRepositoryProvider),
|
||||
ref.watch(backupRepositoryProvider),
|
||||
ref.watch(storageRepositoryProvider),
|
||||
ref.watch(localAssetRepository),
|
||||
ref.watch(appSettingsServiceProvider),
|
||||
ref.watch(assetMediaRepositoryProvider),
|
||||
);
|
||||
|
||||
ref.onDispose(service.dispose);
|
||||
final service = UploadService(ref.watch(backupRepositoryProvider));
|
||||
return service;
|
||||
});
|
||||
|
||||
class UploadService {
|
||||
UploadService(
|
||||
this._uploadRepository,
|
||||
this._backupRepository,
|
||||
this._storageRepository,
|
||||
this._localAssetRepository,
|
||||
this._appSettingsService,
|
||||
this._assetMediaRepository,
|
||||
) {
|
||||
_uploadRepository.onUploadStatus = _onUploadCallback;
|
||||
_uploadRepository.onTaskProgress = _onTaskProgressCallback;
|
||||
}
|
||||
final Stream<UploadApiTaskStatus> taskStatusStream;
|
||||
final Stream<UploadApiTaskProgress> taskProgressStream;
|
||||
UploadService(this._backupRepository) : taskStatusStream = streamStatus(), taskProgressStream = streamProgress();
|
||||
|
||||
final UploadRepository _uploadRepository;
|
||||
final DriftBackupRepository _backupRepository;
|
||||
final StorageRepository _storageRepository;
|
||||
final DriftLocalAssetRepository _localAssetRepository;
|
||||
final AppSettingsService _appSettingsService;
|
||||
final AssetMediaRepository _assetMediaRepository;
|
||||
final Logger _logger = Logger('UploadService');
|
||||
|
||||
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
|
||||
final StreamController<TaskProgressUpdate> _taskProgressController = StreamController<TaskProgressUpdate>.broadcast();
|
||||
|
||||
Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream;
|
||||
Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream;
|
||||
|
||||
bool shouldAbortQueuingTasks = false;
|
||||
|
||||
void _onTaskProgressCallback(TaskProgressUpdate update) {
|
||||
if (!_taskProgressController.isClosed) {
|
||||
_taskProgressController.add(update);
|
||||
}
|
||||
}
|
||||
|
||||
void _onUploadCallback(TaskStatusUpdate update) {
|
||||
if (!_taskStatusController.isClosed) {
|
||||
_taskStatusController.add(update);
|
||||
}
|
||||
_handleTaskStatusUpdate(update);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_taskStatusController.close();
|
||||
_taskProgressController.close();
|
||||
}
|
||||
|
||||
Future<List<bool>> enqueueTasks(List<UploadTask> tasks) {
|
||||
return _uploadRepository.enqueueBackgroundAll(tasks);
|
||||
}
|
||||
|
||||
Future<List<Task>> getActiveTasks(String group) {
|
||||
return _uploadRepository.getActiveTasks(group);
|
||||
}
|
||||
|
||||
Future<({int total, int remainder, int processing})> getBackupCounts(String userId) {
|
||||
return _backupRepository.getAllCounts(userId);
|
||||
}
|
||||
|
||||
Future<void> manualBackup(List<LocalAsset> localAssets) async {
|
||||
await _storageRepository.clearCache();
|
||||
List<UploadTask> tasks = [];
|
||||
for (final asset in localAssets) {
|
||||
final task = await getUploadTask(
|
||||
asset,
|
||||
group: kManualUploadGroup,
|
||||
priority: 1, // High priority after upload motion photo part
|
||||
);
|
||||
if (task != null) {
|
||||
tasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.isNotEmpty) {
|
||||
await enqueueTasks(tasks);
|
||||
}
|
||||
Future<void> manualBackup(List<LocalAsset> localAssets) {
|
||||
return uploadApi.enqueueAssets(localAssets.map((e) => e.id).toList(growable: false));
|
||||
}
|
||||
|
||||
/// Find backup candidates
|
||||
/// Build the upload tasks
|
||||
/// Enqueue the tasks
|
||||
Future<void> startBackup(String userId, void Function(EnqueueStatus status) onEnqueueTasks) async {
|
||||
await _storageRepository.clearCache();
|
||||
|
||||
shouldAbortQueuingTasks = false;
|
||||
|
||||
final candidates = await _backupRepository.getCandidates(userId);
|
||||
if (candidates.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batchSize = 100;
|
||||
int count = 0;
|
||||
for (int i = 0; i < candidates.length; i += batchSize) {
|
||||
if (shouldAbortQueuingTasks) {
|
||||
break;
|
||||
}
|
||||
|
||||
final batch = candidates.skip(i).take(batchSize).toList();
|
||||
List<UploadTask> tasks = [];
|
||||
for (final asset in batch) {
|
||||
final task = await getUploadTask(asset);
|
||||
if (task != null) {
|
||||
tasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
|
||||
count += tasks.length;
|
||||
await enqueueTasks(tasks);
|
||||
|
||||
onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async {
|
||||
await _storageRepository.clearCache();
|
||||
|
||||
shouldAbortQueuingTasks = false;
|
||||
|
||||
final candidates = await _backupRepository.getCandidates(userId);
|
||||
if (candidates.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
const batchSize = 100;
|
||||
for (int i = 0; i < candidates.length; i += batchSize) {
|
||||
if (shouldAbortQueuingTasks || token.isCancelled) {
|
||||
break;
|
||||
}
|
||||
|
||||
final batch = candidates.skip(i).take(batchSize).toList();
|
||||
List<UploadTaskWithFile> tasks = [];
|
||||
for (final asset in batch) {
|
||||
final requireWifi = _shouldRequireWiFi(asset);
|
||||
if (requireWifi && !hasWifi) {
|
||||
_logger.warning('Skipping upload for ${asset.id} because it requires WiFi');
|
||||
continue;
|
||||
}
|
||||
|
||||
final task = await _getUploadTaskWithFile(asset);
|
||||
if (task != null) {
|
||||
tasks.add(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
|
||||
await _uploadRepository.backupWithDartClient(tasks, token);
|
||||
}
|
||||
}
|
||||
Future<void> startBackup() async {
|
||||
return uploadApi.refresh();
|
||||
}
|
||||
|
||||
/// Cancel all ongoing uploads and reset the upload queue
|
||||
///
|
||||
/// Return the number of left over tasks in the queue
|
||||
Future<int> cancelBackup() async {
|
||||
shouldAbortQueuingTasks = true;
|
||||
|
||||
await _storageRepository.clearCache();
|
||||
await _uploadRepository.reset(kBackupGroup);
|
||||
await _uploadRepository.deleteDatabaseRecords(kBackupGroup);
|
||||
|
||||
final activeTasks = await _uploadRepository.getActiveTasks(kBackupGroup);
|
||||
return activeTasks.length;
|
||||
}
|
||||
|
||||
Future<void> resumeBackup() {
|
||||
return _uploadRepository.start();
|
||||
}
|
||||
|
||||
void _handleTaskStatusUpdate(TaskStatusUpdate update) async {
|
||||
switch (update.status) {
|
||||
case TaskStatus.complete:
|
||||
unawaited(_handleLivePhoto(update));
|
||||
|
||||
if (CurrentPlatform.isIOS) {
|
||||
try {
|
||||
final path = await update.task.filePath();
|
||||
await File(path).delete();
|
||||
} catch (e) {
|
||||
_logger.severe('Error deleting file path for iOS: $e');
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleLivePhoto(TaskStatusUpdate update) async {
|
||||
try {
|
||||
if (update.task.metaData.isEmpty || update.task.metaData == '') {
|
||||
return;
|
||||
}
|
||||
|
||||
final metadata = UploadTaskMetadata.fromJson(update.task.metaData);
|
||||
if (!metadata.isLivePhotos) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (update.responseBody == null || update.responseBody!.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final response = jsonDecode(update.responseBody!);
|
||||
|
||||
final localAsset = await _localAssetRepository.getById(metadata.localAssetId);
|
||||
if (localAsset == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final uploadTask = await getLivePhotoUploadTask(localAsset, response['id'] as String);
|
||||
|
||||
if (uploadTask == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await enqueueTasks([uploadTask]);
|
||||
} catch (error, stackTrace) {
|
||||
dPrint(() => "Error handling live photo upload task: $error $stackTrace");
|
||||
}
|
||||
}
|
||||
|
||||
Future<UploadTaskWithFile?> _getUploadTaskWithFile(LocalAsset asset) async {
|
||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final originalFileName = entity.isLivePhoto ? p.setExtension(asset.name, p.extension(file.path)) : asset.name;
|
||||
|
||||
String metadata = UploadTaskMetadata(
|
||||
localAssetId: asset.id,
|
||||
isLivePhotos: entity.isLivePhoto,
|
||||
livePhotoVideoId: '',
|
||||
).toJson();
|
||||
|
||||
return UploadTaskWithFile(
|
||||
file: file,
|
||||
task: await buildUploadTask(
|
||||
file,
|
||||
createdAt: asset.createdAt,
|
||||
modifiedAt: asset.updatedAt,
|
||||
originalFileName: originalFileName,
|
||||
deviceAssetId: asset.id,
|
||||
metadata: metadata,
|
||||
group: "group",
|
||||
priority: 0,
|
||||
isFavorite: asset.isFavorite,
|
||||
requiresWiFi: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<UploadTask?> getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async {
|
||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
File? file;
|
||||
|
||||
/// iOS LivePhoto has two files: a photo and a video.
|
||||
/// They are uploaded separately, with video file being upload first, then returned with the assetId
|
||||
/// The assetId is then used as a metadata for the photo file upload task.
|
||||
///
|
||||
/// We implement two separate upload groups for this, the normal one for the video file
|
||||
/// and the higher priority group for the photo file because the video file is already uploaded.
|
||||
///
|
||||
/// The cancel operation will only cancel the video group (normal group), the photo group will not
|
||||
/// be touched, as the video file is already uploaded.
|
||||
|
||||
if (entity.isLivePhoto) {
|
||||
file = await _storageRepository.getMotionFileForAsset(asset);
|
||||
} else {
|
||||
file = await _storageRepository.getFileForAsset(asset.id);
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
final originalFileName = entity.isLivePhoto ? p.setExtension(fileName, p.extension(file.path)) : fileName;
|
||||
|
||||
String metadata = UploadTaskMetadata(
|
||||
localAssetId: asset.id,
|
||||
isLivePhotos: entity.isLivePhoto,
|
||||
livePhotoVideoId: '',
|
||||
).toJson();
|
||||
|
||||
final requiresWiFi = _shouldRequireWiFi(asset);
|
||||
|
||||
return buildUploadTask(
|
||||
file,
|
||||
createdAt: asset.createdAt,
|
||||
modifiedAt: asset.updatedAt,
|
||||
originalFileName: originalFileName,
|
||||
deviceAssetId: asset.id,
|
||||
metadata: metadata,
|
||||
group: group,
|
||||
priority: priority,
|
||||
isFavorite: asset.isFavorite,
|
||||
requiresWiFi: requiresWiFi,
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<UploadTask?> getLivePhotoUploadTask(LocalAsset asset, String livePhotoVideoId) async {
|
||||
final entity = await _storageRepository.getAssetEntityForAsset(asset);
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final file = await _storageRepository.getFileForAsset(asset.id);
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fields = {'livePhotoVideoId': livePhotoVideoId};
|
||||
|
||||
final requiresWiFi = _shouldRequireWiFi(asset);
|
||||
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
|
||||
return buildUploadTask(
|
||||
file,
|
||||
createdAt: asset.createdAt,
|
||||
modifiedAt: asset.updatedAt,
|
||||
originalFileName: originalFileName,
|
||||
deviceAssetId: asset.id,
|
||||
fields: fields,
|
||||
group: kBackupLivePhotoGroup,
|
||||
priority: 0, // Highest priority to get upload immediately
|
||||
isFavorite: asset.isFavorite,
|
||||
requiresWiFi: requiresWiFi,
|
||||
);
|
||||
}
|
||||
|
||||
bool _shouldRequireWiFi(LocalAsset asset) {
|
||||
bool requiresWiFi = true;
|
||||
|
||||
if (asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)) {
|
||||
requiresWiFi = false;
|
||||
} else if (!asset.isVideo && _appSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)) {
|
||||
requiresWiFi = false;
|
||||
}
|
||||
|
||||
return requiresWiFi;
|
||||
}
|
||||
|
||||
Future<UploadTask> buildUploadTask(
|
||||
File file, {
|
||||
required String group,
|
||||
required DateTime createdAt,
|
||||
required DateTime modifiedAt,
|
||||
Map<String, String>? fields,
|
||||
String? originalFileName,
|
||||
String? deviceAssetId,
|
||||
String? metadata,
|
||||
int? priority,
|
||||
bool? isFavorite,
|
||||
bool requiresWiFi = true,
|
||||
}) async {
|
||||
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
|
||||
final url = Uri.parse('$serverEndpoint/assets').toString();
|
||||
final headers = ApiService.getRequestHeaders();
|
||||
final deviceId = Store.get(StoreKey.deviceId);
|
||||
final (baseDirectory, directory, filename) = await Task.split(filePath: file.path);
|
||||
final fieldsMap = {
|
||||
'filename': originalFileName ?? filename,
|
||||
'deviceAssetId': deviceAssetId ?? '',
|
||||
'deviceId': deviceId,
|
||||
'fileCreatedAt': createdAt.toUtc().toIso8601String(),
|
||||
'fileModifiedAt': modifiedAt.toUtc().toIso8601String(),
|
||||
'isFavorite': isFavorite?.toString() ?? 'false',
|
||||
'duration': '0',
|
||||
if (fields != null) ...fields,
|
||||
};
|
||||
|
||||
return UploadTask(
|
||||
taskId: deviceAssetId,
|
||||
displayName: originalFileName ?? filename,
|
||||
httpRequestMethod: 'POST',
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: filename,
|
||||
fields: fieldsMap,
|
||||
baseDirectory: baseDirectory,
|
||||
directory: directory,
|
||||
fileField: 'assetData',
|
||||
metaData: metadata ?? '',
|
||||
group: group,
|
||||
requiresWiFi: requiresWiFi,
|
||||
priority: priority ?? 5,
|
||||
updates: Updates.statusAndProgress,
|
||||
retries: 3,
|
||||
);
|
||||
Future<void> cancelBackup() {
|
||||
return uploadApi.cancelAll();
|
||||
}
|
||||
}
|
||||
|
||||
class UploadTaskMetadata {
|
||||
final String localAssetId;
|
||||
final bool isLivePhotos;
|
||||
final String livePhotoVideoId;
|
||||
|
||||
const UploadTaskMetadata({required this.localAssetId, required this.isLivePhotos, required this.livePhotoVideoId});
|
||||
|
||||
UploadTaskMetadata copyWith({String? localAssetId, bool? isLivePhotos, String? livePhotoVideoId}) {
|
||||
return UploadTaskMetadata(
|
||||
localAssetId: localAssetId ?? this.localAssetId,
|
||||
isLivePhotos: isLivePhotos ?? this.isLivePhotos,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
return <String, dynamic>{
|
||||
'localAssetId': localAssetId,
|
||||
'isLivePhotos': isLivePhotos,
|
||||
'livePhotoVideoId': livePhotoVideoId,
|
||||
};
|
||||
}
|
||||
|
||||
factory UploadTaskMetadata.fromMap(Map<String, dynamic> map) {
|
||||
return UploadTaskMetadata(
|
||||
localAssetId: map['localAssetId'] as String,
|
||||
isLivePhotos: map['isLivePhotos'] as bool,
|
||||
livePhotoVideoId: map['livePhotoVideoId'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
String toJson() => json.encode(toMap());
|
||||
|
||||
factory UploadTaskMetadata.fromJson(String source) =>
|
||||
UploadTaskMetadata.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'UploadTaskMetadata(localAssetId: $localAssetId, isLivePhotos: $isLivePhotos, livePhotoVideoId: $livePhotoVideoId)';
|
||||
|
||||
@override
|
||||
bool operator ==(covariant UploadTaskMetadata other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other.localAssetId == localAssetId &&
|
||||
other.isLivePhotos == isLivePhotos &&
|
||||
other.livePhotoVideoId == livePhotoVideoId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => localAssetId.hashCode ^ isLivePhotos.hashCode ^ livePhotoVideoId.hashCode;
|
||||
}
|
||||
|
||||
@@ -39,20 +39,6 @@ void configureFileDownloaderNotifications() {
|
||||
complete: TaskNotification('download_finished'.t(), '${'file_name'.t()}: {filename}'),
|
||||
progressBar: true,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kManualUploadGroup,
|
||||
running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()),
|
||||
complete: TaskNotification('upload_finished'.t(), 'backup_background_service_complete_notification'.t()),
|
||||
groupNotificationId: kManualUploadGroup,
|
||||
);
|
||||
|
||||
FileDownloader().configureNotificationForGroup(
|
||||
kBackupGroup,
|
||||
running: TaskNotification('uploading_media'.t(), 'backup_background_service_in_progress_notification'.t()),
|
||||
complete: TaskNotification('upload_finished'.t(), 'backup_background_service_complete_notification'.t()),
|
||||
groupNotificationId: kBackupGroup,
|
||||
);
|
||||
}
|
||||
|
||||
abstract final class Bootstrap {
|
||||
|
||||
Reference in New Issue
Block a user