Compare commits

...

6 Commits

Author SHA1 Message Date
shenlong-tanwen
c8e6080ddc replace trigger with manual pruning 2025-11-08 01:23:13 +05:30
shenlong-tanwen
70300ad3d4 ignore failed uploads from being retried on timer 2025-11-06 21:18:35 +05:30
shenlong-tanwen
91fe3c8d96 fetch failed uploads from local asset upload entity 2025-11-06 20:54:09 +05:30
Alex
3b6e23fed7 Merge branch 'main' into fix/periodic-batch-enqueue 2025-11-05 13:29:28 -06:00
shenlong-tanwen
d37cae5dcd track asset upload status and sort upload queue 2025-11-05 18:00:37 +05:30
shenlong-tanwen
94044c98bf fix: enqueue assets in batches for uploads 2025-11-05 18:00:37 +05:30
19 changed files with 9184 additions and 147 deletions

File diff suppressed because one or more lines are too long

View File

@@ -7,3 +7,5 @@ enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id }
enum ActionSource { timeline, viewer }
enum UploadErrorType { none, network, client, server, unknown }

View File

@@ -0,0 +1,18 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
class LocalAssetUploadEntity extends Table with DriftDefaultsMixin {
const LocalAssetUploadEntity();
TextColumn get assetId => text().references(LocalAssetEntity, #id, onDelete: KeyAction.cascade)();
IntColumn get numberOfAttempts => integer().withDefault(const Constant(0))();
DateTimeColumn get lastAttemptAt => dateTime().withDefault(currentDateAndTime)();
IntColumn get errorType => intEnum<UploadErrorType>().withDefault(const Constant(0))();
TextColumn get errorMessage => text().nullable()();
@override
Set<Column> get primaryKey => {assetId};
}

View File

@@ -0,0 +1,783 @@
// dart format width=80
// ignore_for_file: type=lint
import 'package:drift/drift.dart' as i0;
import 'package:immich_mobile/infrastructure/entities/local_asset_upload_entity.drift.dart'
as i1;
import 'package:immich_mobile/constants/enums.dart' as i2;
import 'package:immich_mobile/infrastructure/entities/local_asset_upload_entity.dart'
as i3;
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'
as i5;
import 'package:drift/internal/modular.dart' as i6;
typedef $$LocalAssetUploadEntityTableCreateCompanionBuilder =
i1.LocalAssetUploadEntityCompanion Function({
required String assetId,
i0.Value<int> numberOfAttempts,
i0.Value<DateTime> lastAttemptAt,
i0.Value<i2.UploadErrorType> errorType,
i0.Value<String?> errorMessage,
});
typedef $$LocalAssetUploadEntityTableUpdateCompanionBuilder =
i1.LocalAssetUploadEntityCompanion Function({
i0.Value<String> assetId,
i0.Value<int> numberOfAttempts,
i0.Value<DateTime> lastAttemptAt,
i0.Value<i2.UploadErrorType> errorType,
i0.Value<String?> errorMessage,
});
final class $$LocalAssetUploadEntityTableReferences
extends
i0.BaseReferences<
i0.GeneratedDatabase,
i1.$LocalAssetUploadEntityTable,
i1.LocalAssetUploadEntityData
> {
$$LocalAssetUploadEntityTableReferences(
super.$_db,
super.$_table,
super.$_typedResult,
);
static i5.$LocalAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
i6.ReadDatabaseContainer(db)
.resultSet<i5.$LocalAssetEntityTable>('local_asset_entity')
.createAlias(
i0.$_aliasNameGenerator(
i6.ReadDatabaseContainer(db)
.resultSet<i1.$LocalAssetUploadEntityTable>(
'local_asset_upload_entity',
)
.assetId,
i6.ReadDatabaseContainer(
db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity').id,
),
);
i5.$$LocalAssetEntityTableProcessedTableManager get assetId {
final $_column = $_itemColumn<String>('asset_id')!;
final manager = i5
.$$LocalAssetEntityTableTableManager(
$_db,
i6.ReadDatabaseContainer(
$_db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
)
.filter((f) => f.id.sqlEquals($_column));
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
if (item == null) return manager;
return i0.ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]),
);
}
}
class $$LocalAssetUploadEntityTableFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetUploadEntityTable> {
$$LocalAssetUploadEntityTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<int> get numberOfAttempts => $composableBuilder(
column: $table.numberOfAttempts,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get lastAttemptAt => $composableBuilder(
column: $table.lastAttemptAt,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnWithTypeConverterFilters<i2.UploadErrorType, i2.UploadErrorType, int>
get errorType => $composableBuilder(
column: $table.errorType,
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
);
i0.ColumnFilters<String> get errorMessage => $composableBuilder(
column: $table.errorMessage,
builder: (column) => i0.ColumnFilters(column),
);
i5.$$LocalAssetEntityTableFilterComposer get assetId {
final i5.$$LocalAssetEntityTableFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$LocalAssetEntityTableFilterComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalAssetUploadEntityTableOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetUploadEntityTable> {
$$LocalAssetUploadEntityTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<int> get numberOfAttempts => $composableBuilder(
column: $table.numberOfAttempts,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get lastAttemptAt => $composableBuilder(
column: $table.lastAttemptAt,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<int> get errorType => $composableBuilder(
column: $table.errorType,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<String> get errorMessage => $composableBuilder(
column: $table.errorMessage,
builder: (column) => i0.ColumnOrderings(column),
);
i5.$$LocalAssetEntityTableOrderingComposer get assetId {
final i5.$$LocalAssetEntityTableOrderingComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$LocalAssetEntityTableOrderingComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalAssetUploadEntityTableAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i1.$LocalAssetUploadEntityTable> {
$$LocalAssetUploadEntityTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<int> get numberOfAttempts => $composableBuilder(
column: $table.numberOfAttempts,
builder: (column) => column,
);
i0.GeneratedColumn<DateTime> get lastAttemptAt => $composableBuilder(
column: $table.lastAttemptAt,
builder: (column) => column,
);
i0.GeneratedColumnWithTypeConverter<i2.UploadErrorType, int> get errorType =>
$composableBuilder(column: $table.errorType, builder: (column) => column);
i0.GeneratedColumn<String> get errorMessage => $composableBuilder(
column: $table.errorMessage,
builder: (column) => column,
);
i5.$$LocalAssetEntityTableAnnotationComposer get assetId {
final i5.$$LocalAssetEntityTableAnnotationComposer composer =
$composerBuilder(
composer: this,
getCurrentColumn: (t) => t.assetId,
referencedTable: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
getReferencedColumn: (t) => t.id,
builder:
(
joinBuilder, {
$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer,
}) => i5.$$LocalAssetEntityTableAnnotationComposer(
$db: $db,
$table: i6.ReadDatabaseContainer(
$db,
).resultSet<i5.$LocalAssetEntityTable>('local_asset_entity'),
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
),
);
return composer;
}
}
class $$LocalAssetUploadEntityTableTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i1.$LocalAssetUploadEntityTable,
i1.LocalAssetUploadEntityData,
i1.$$LocalAssetUploadEntityTableFilterComposer,
i1.$$LocalAssetUploadEntityTableOrderingComposer,
i1.$$LocalAssetUploadEntityTableAnnotationComposer,
$$LocalAssetUploadEntityTableCreateCompanionBuilder,
$$LocalAssetUploadEntityTableUpdateCompanionBuilder,
(
i1.LocalAssetUploadEntityData,
i1.$$LocalAssetUploadEntityTableReferences,
),
i1.LocalAssetUploadEntityData,
i0.PrefetchHooks Function({bool assetId})
> {
$$LocalAssetUploadEntityTableTableManager(
i0.GeneratedDatabase db,
i1.$LocalAssetUploadEntityTable table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i1.$$LocalAssetUploadEntityTableFilterComposer(
$db: db,
$table: table,
),
createOrderingComposer: () =>
i1.$$LocalAssetUploadEntityTableOrderingComposer(
$db: db,
$table: table,
),
createComputedFieldComposer: () =>
i1.$$LocalAssetUploadEntityTableAnnotationComposer(
$db: db,
$table: table,
),
updateCompanionCallback:
({
i0.Value<String> assetId = const i0.Value.absent(),
i0.Value<int> numberOfAttempts = const i0.Value.absent(),
i0.Value<DateTime> lastAttemptAt = const i0.Value.absent(),
i0.Value<i2.UploadErrorType> errorType =
const i0.Value.absent(),
i0.Value<String?> errorMessage = const i0.Value.absent(),
}) => i1.LocalAssetUploadEntityCompanion(
assetId: assetId,
numberOfAttempts: numberOfAttempts,
lastAttemptAt: lastAttemptAt,
errorType: errorType,
errorMessage: errorMessage,
),
createCompanionCallback:
({
required String assetId,
i0.Value<int> numberOfAttempts = const i0.Value.absent(),
i0.Value<DateTime> lastAttemptAt = const i0.Value.absent(),
i0.Value<i2.UploadErrorType> errorType =
const i0.Value.absent(),
i0.Value<String?> errorMessage = const i0.Value.absent(),
}) => i1.LocalAssetUploadEntityCompanion.insert(
assetId: assetId,
numberOfAttempts: numberOfAttempts,
lastAttemptAt: lastAttemptAt,
errorType: errorType,
errorMessage: errorMessage,
),
withReferenceMapper: (p0) => p0
.map(
(e) => (
e.readTable(table),
i1.$$LocalAssetUploadEntityTableReferences(db, table, e),
),
)
.toList(),
prefetchHooksCallback: ({assetId = false}) {
return i0.PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins:
<
T extends i0.TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic
>
>(state) {
if (assetId) {
state =
state.withJoin(
currentTable: table,
currentColumn: table.assetId,
referencedTable: i1
.$$LocalAssetUploadEntityTableReferences
._assetIdTable(db),
referencedColumn: i1
.$$LocalAssetUploadEntityTableReferences
._assetIdTable(db)
.id,
)
as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
),
);
}
typedef $$LocalAssetUploadEntityTableProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i1.$LocalAssetUploadEntityTable,
i1.LocalAssetUploadEntityData,
i1.$$LocalAssetUploadEntityTableFilterComposer,
i1.$$LocalAssetUploadEntityTableOrderingComposer,
i1.$$LocalAssetUploadEntityTableAnnotationComposer,
$$LocalAssetUploadEntityTableCreateCompanionBuilder,
$$LocalAssetUploadEntityTableUpdateCompanionBuilder,
(
i1.LocalAssetUploadEntityData,
i1.$$LocalAssetUploadEntityTableReferences,
),
i1.LocalAssetUploadEntityData,
i0.PrefetchHooks Function({bool assetId})
>;
class $LocalAssetUploadEntityTable extends i3.LocalAssetUploadEntity
with
i0.TableInfo<
$LocalAssetUploadEntityTable,
i1.LocalAssetUploadEntityData
> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalAssetUploadEntityTable(this.attachedDatabase, [this._alias]);
static const i0.VerificationMeta _assetIdMeta = const i0.VerificationMeta(
'assetId',
);
@override
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
'asset_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
'REFERENCES local_asset_entity (id) ON DELETE CASCADE',
),
);
static const i0.VerificationMeta _numberOfAttemptsMeta =
const i0.VerificationMeta('numberOfAttempts');
@override
late final i0.GeneratedColumn<int> numberOfAttempts = i0.GeneratedColumn<int>(
'number_of_attempts',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
);
static const i0.VerificationMeta _lastAttemptAtMeta =
const i0.VerificationMeta('lastAttemptAt');
@override
late final i0.GeneratedColumn<DateTime> lastAttemptAt =
i0.GeneratedColumn<DateTime>(
'last_attempt_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: i4.currentDateAndTime,
);
@override
late final i0.GeneratedColumnWithTypeConverter<i2.UploadErrorType, int>
errorType =
i0.GeneratedColumn<int>(
'error_type',
aliasedName,
false,
type: i0.DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const i4.Constant(0),
).withConverter<i2.UploadErrorType>(
i1.$LocalAssetUploadEntityTable.$convertererrorType,
);
static const i0.VerificationMeta _errorMessageMeta =
const i0.VerificationMeta('errorMessage');
@override
late final i0.GeneratedColumn<String> errorMessage =
i0.GeneratedColumn<String>(
'error_message',
aliasedName,
true,
type: i0.DriftSqlType.string,
requiredDuringInsert: false,
);
@override
List<i0.GeneratedColumn> get $columns => [
assetId,
numberOfAttempts,
lastAttemptAt,
errorType,
errorMessage,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_asset_upload_entity';
@override
i0.VerificationContext validateIntegrity(
i0.Insertable<i1.LocalAssetUploadEntityData> instance, {
bool isInserting = false,
}) {
final context = i0.VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('asset_id')) {
context.handle(
_assetIdMeta,
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta),
);
} else if (isInserting) {
context.missing(_assetIdMeta);
}
if (data.containsKey('number_of_attempts')) {
context.handle(
_numberOfAttemptsMeta,
numberOfAttempts.isAcceptableOrUnknown(
data['number_of_attempts']!,
_numberOfAttemptsMeta,
),
);
}
if (data.containsKey('last_attempt_at')) {
context.handle(
_lastAttemptAtMeta,
lastAttemptAt.isAcceptableOrUnknown(
data['last_attempt_at']!,
_lastAttemptAtMeta,
),
);
}
if (data.containsKey('error_message')) {
context.handle(
_errorMessageMeta,
errorMessage.isAcceptableOrUnknown(
data['error_message']!,
_errorMessageMeta,
),
);
}
return context;
}
@override
Set<i0.GeneratedColumn> get $primaryKey => {assetId};
@override
i1.LocalAssetUploadEntityData map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i1.LocalAssetUploadEntityData(
assetId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}asset_id'],
)!,
numberOfAttempts: attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}number_of_attempts'],
)!,
lastAttemptAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}last_attempt_at'],
)!,
errorType: i1.$LocalAssetUploadEntityTable.$convertererrorType.fromSql(
attachedDatabase.typeMapping.read(
i0.DriftSqlType.int,
data['${effectivePrefix}error_type'],
)!,
),
errorMessage: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}error_message'],
),
);
}
@override
$LocalAssetUploadEntityTable createAlias(String alias) {
return $LocalAssetUploadEntityTable(attachedDatabase, alias);
}
static i0.JsonTypeConverter2<i2.UploadErrorType, int, int>
$convertererrorType = const i0.EnumIndexConverter<i2.UploadErrorType>(
i2.UploadErrorType.values,
);
@override
bool get withoutRowId => true;
@override
bool get isStrict => true;
}
class LocalAssetUploadEntityData extends i0.DataClass
implements i0.Insertable<i1.LocalAssetUploadEntityData> {
final String assetId;
final int numberOfAttempts;
final DateTime lastAttemptAt;
final i2.UploadErrorType errorType;
final String? errorMessage;
const LocalAssetUploadEntityData({
required this.assetId,
required this.numberOfAttempts,
required this.lastAttemptAt,
required this.errorType,
this.errorMessage,
});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['asset_id'] = i0.Variable<String>(assetId);
map['number_of_attempts'] = i0.Variable<int>(numberOfAttempts);
map['last_attempt_at'] = i0.Variable<DateTime>(lastAttemptAt);
{
map['error_type'] = i0.Variable<int>(
i1.$LocalAssetUploadEntityTable.$convertererrorType.toSql(errorType),
);
}
if (!nullToAbsent || errorMessage != null) {
map['error_message'] = i0.Variable<String>(errorMessage);
}
return map;
}
factory LocalAssetUploadEntityData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return LocalAssetUploadEntityData(
assetId: serializer.fromJson<String>(json['assetId']),
numberOfAttempts: serializer.fromJson<int>(json['numberOfAttempts']),
lastAttemptAt: serializer.fromJson<DateTime>(json['lastAttemptAt']),
errorType: i1.$LocalAssetUploadEntityTable.$convertererrorType.fromJson(
serializer.fromJson<int>(json['errorType']),
),
errorMessage: serializer.fromJson<String?>(json['errorMessage']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'assetId': serializer.toJson<String>(assetId),
'numberOfAttempts': serializer.toJson<int>(numberOfAttempts),
'lastAttemptAt': serializer.toJson<DateTime>(lastAttemptAt),
'errorType': serializer.toJson<int>(
i1.$LocalAssetUploadEntityTable.$convertererrorType.toJson(errorType),
),
'errorMessage': serializer.toJson<String?>(errorMessage),
};
}
i1.LocalAssetUploadEntityData copyWith({
String? assetId,
int? numberOfAttempts,
DateTime? lastAttemptAt,
i2.UploadErrorType? errorType,
i0.Value<String?> errorMessage = const i0.Value.absent(),
}) => i1.LocalAssetUploadEntityData(
assetId: assetId ?? this.assetId,
numberOfAttempts: numberOfAttempts ?? this.numberOfAttempts,
lastAttemptAt: lastAttemptAt ?? this.lastAttemptAt,
errorType: errorType ?? this.errorType,
errorMessage: errorMessage.present ? errorMessage.value : this.errorMessage,
);
LocalAssetUploadEntityData copyWithCompanion(
i1.LocalAssetUploadEntityCompanion data,
) {
return LocalAssetUploadEntityData(
assetId: data.assetId.present ? data.assetId.value : this.assetId,
numberOfAttempts: data.numberOfAttempts.present
? data.numberOfAttempts.value
: this.numberOfAttempts,
lastAttemptAt: data.lastAttemptAt.present
? data.lastAttemptAt.value
: this.lastAttemptAt,
errorType: data.errorType.present ? data.errorType.value : this.errorType,
errorMessage: data.errorMessage.present
? data.errorMessage.value
: this.errorMessage,
);
}
@override
String toString() {
return (StringBuffer('LocalAssetUploadEntityData(')
..write('assetId: $assetId, ')
..write('numberOfAttempts: $numberOfAttempts, ')
..write('lastAttemptAt: $lastAttemptAt, ')
..write('errorType: $errorType, ')
..write('errorMessage: $errorMessage')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(
assetId,
numberOfAttempts,
lastAttemptAt,
errorType,
errorMessage,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i1.LocalAssetUploadEntityData &&
other.assetId == this.assetId &&
other.numberOfAttempts == this.numberOfAttempts &&
other.lastAttemptAt == this.lastAttemptAt &&
other.errorType == this.errorType &&
other.errorMessage == this.errorMessage);
}
class LocalAssetUploadEntityCompanion
extends i0.UpdateCompanion<i1.LocalAssetUploadEntityData> {
final i0.Value<String> assetId;
final i0.Value<int> numberOfAttempts;
final i0.Value<DateTime> lastAttemptAt;
final i0.Value<i2.UploadErrorType> errorType;
final i0.Value<String?> errorMessage;
const LocalAssetUploadEntityCompanion({
this.assetId = const i0.Value.absent(),
this.numberOfAttempts = const i0.Value.absent(),
this.lastAttemptAt = const i0.Value.absent(),
this.errorType = const i0.Value.absent(),
this.errorMessage = const i0.Value.absent(),
});
LocalAssetUploadEntityCompanion.insert({
required String assetId,
this.numberOfAttempts = const i0.Value.absent(),
this.lastAttemptAt = const i0.Value.absent(),
this.errorType = const i0.Value.absent(),
this.errorMessage = const i0.Value.absent(),
}) : assetId = i0.Value(assetId);
static i0.Insertable<i1.LocalAssetUploadEntityData> custom({
i0.Expression<String>? assetId,
i0.Expression<int>? numberOfAttempts,
i0.Expression<DateTime>? lastAttemptAt,
i0.Expression<int>? errorType,
i0.Expression<String>? errorMessage,
}) {
return i0.RawValuesInsertable({
if (assetId != null) 'asset_id': assetId,
if (numberOfAttempts != null) 'number_of_attempts': numberOfAttempts,
if (lastAttemptAt != null) 'last_attempt_at': lastAttemptAt,
if (errorType != null) 'error_type': errorType,
if (errorMessage != null) 'error_message': errorMessage,
});
}
i1.LocalAssetUploadEntityCompanion copyWith({
i0.Value<String>? assetId,
i0.Value<int>? numberOfAttempts,
i0.Value<DateTime>? lastAttemptAt,
i0.Value<i2.UploadErrorType>? errorType,
i0.Value<String?>? errorMessage,
}) {
return i1.LocalAssetUploadEntityCompanion(
assetId: assetId ?? this.assetId,
numberOfAttempts: numberOfAttempts ?? this.numberOfAttempts,
lastAttemptAt: lastAttemptAt ?? this.lastAttemptAt,
errorType: errorType ?? this.errorType,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (assetId.present) {
map['asset_id'] = i0.Variable<String>(assetId.value);
}
if (numberOfAttempts.present) {
map['number_of_attempts'] = i0.Variable<int>(numberOfAttempts.value);
}
if (lastAttemptAt.present) {
map['last_attempt_at'] = i0.Variable<DateTime>(lastAttemptAt.value);
}
if (errorType.present) {
map['error_type'] = i0.Variable<int>(
i1.$LocalAssetUploadEntityTable.$convertererrorType.toSql(
errorType.value,
),
);
}
if (errorMessage.present) {
map['error_message'] = i0.Variable<String>(errorMessage.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalAssetUploadEntityCompanion(')
..write('assetId: $assetId, ')
..write('numberOfAttempts: $numberOfAttempts, ')
..write('lastAttemptAt: $lastAttemptAt, ')
..write('errorType: $errorType, ')
..write('errorMessage: $errorMessage')
..write(')'))
.toString();
}
}

View File

@@ -8,6 +8,8 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
enum SortCandidatesBy { createdAt, attemptCount }
final backupRepositoryProvider = Provider<DriftBackupRepository>(
(ref) => DriftBackupRepository(ref.watch(driftProvider)),
);
@@ -81,37 +83,67 @@ class DriftBackupRepository extends DriftDatabaseRepository {
);
}
Future<List<LocalAsset>> getCandidates(String userId, {bool onlyHashed = true}) async {
Future<List<LocalAsset>> getCandidates(
String userId, {
bool onlyHashed = true,
bool ignoreFailed = false,
int? limit,
SortCandidatesBy sortBy = SortCandidatesBy.createdAt,
}) async {
final selectedAlbumIds = _db.localAlbumEntity.selectOnly(distinct: true)
..addColumns([_db.localAlbumEntity.id])
..where(_db.localAlbumEntity.backupSelection.equalsValue(BackupSelection.selected));
final query = _db.localAssetEntity.select()
..where(
(lae) =>
existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(
_db.localAlbumAssetEntity.albumId.isInQuery(selectedAlbumIds) &
_db.localAlbumAssetEntity.assetId.equalsExp(lae.id),
),
) &
notExistsQuery(
_db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.checksum])
..where(
_db.remoteAssetEntity.checksum.equalsExp(lae.checksum) & _db.remoteAssetEntity.ownerId.equals(userId),
),
) &
lae.id.isNotInQuery(_getExcludedSubquery()),
)
..orderBy([(localAsset) => OrderingTerm.desc(localAsset.createdAt)]);
final query =
_db.localAssetEntity.select().join([
leftOuterJoin(
_db.localAssetUploadEntity,
_db.localAssetEntity.id.equalsExp(_db.localAssetUploadEntity.assetId),
useColumns: false,
),
])..where(
existsQuery(
_db.localAlbumAssetEntity.selectOnly()
..addColumns([_db.localAlbumAssetEntity.assetId])
..where(
_db.localAlbumAssetEntity.albumId.isInQuery(selectedAlbumIds) &
_db.localAlbumAssetEntity.assetId.equalsExp(_db.localAssetEntity.id),
),
) &
notExistsQuery(
_db.remoteAssetEntity.selectOnly()
..addColumns([_db.remoteAssetEntity.checksum])
..where(
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum) &
_db.remoteAssetEntity.ownerId.equals(userId),
),
) &
_db.localAssetEntity.id.isNotInQuery(_getExcludedSubquery()),
);
if (onlyHashed) {
query.where((lae) => lae.checksum.isNotNull());
switch (sortBy) {
case SortCandidatesBy.createdAt:
query.orderBy([OrderingTerm.asc(_db.localAssetEntity.createdAt)]);
case SortCandidatesBy.attemptCount:
query.orderBy([
OrderingTerm.asc(_db.localAssetUploadEntity.numberOfAttempts, nulls: NullsOrder.first),
OrderingTerm.asc(_db.localAssetUploadEntity.lastAttemptAt, nulls: NullsOrder.first),
OrderingTerm.asc(_db.localAssetEntity.createdAt),
]);
}
return query.map((localAsset) => localAsset.toDto()).get();
if (onlyHashed) {
query.where(_db.localAssetEntity.checksum.isNotNull());
}
if (ignoreFailed) {
query.where(_db.localAssetUploadEntity.assetId.isNull());
}
if (limit != null) {
query.limit(limit);
}
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
}
}

View File

@@ -10,6 +10,7 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset_upload_entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
@@ -50,6 +51,7 @@ class IsarDatabaseRepository implements IDatabaseRepository {
PartnerEntity,
LocalAlbumEntity,
LocalAssetEntity,
LocalAssetUploadEntity,
LocalAlbumAssetEntity,
RemoteAssetEntity,
RemoteExifEntity,
@@ -93,7 +95,7 @@ class Drift extends $Drift implements IDatabaseRepository {
}
@override
int get schemaVersion => 12;
int get schemaVersion => 13;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -178,6 +180,9 @@ class Drift extends $Drift implements IDatabaseRepository {
);
}
},
from12To13: (m, v13) async {
await m.createTable(v13.localAssetUploadEntity);
},
),
);

View File

@@ -21,25 +21,27 @@ import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.drift
as i9;
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'
as i10;
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'
import 'package:immich_mobile/infrastructure/entities/local_asset_upload_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/merged_asset.drift.dart'
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'
as i19;
import 'package:drift/internal/modular.dart' as i20;
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
as i20;
import 'package:drift/internal/modular.dart' as i21;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -64,22 +66,24 @@ abstract class $Drift extends i0.GeneratedDatabase {
late final i10.$PartnerEntityTable partnerEntity = i10.$PartnerEntityTable(
this,
);
late final i11.$RemoteExifEntityTable remoteExifEntity = i11
late final i11.$LocalAssetUploadEntityTable localAssetUploadEntity = i11
.$LocalAssetUploadEntityTable(this);
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);
i19.MergedAssetDrift get mergedAssetDrift => i20.ReadDatabaseContainer(
late final i19.$StoreEntityTable storeEntity = i19.$StoreEntityTable(this);
i20.MergedAssetDrift get mergedAssetDrift => i21.ReadDatabaseContainer(
this,
).accessor<i19.MergedAssetDrift>(i19.MergedAssetDrift.new);
).accessor<i20.MergedAssetDrift>(i20.MergedAssetDrift.new);
@override
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
@@ -100,6 +104,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
authUserEntity,
userMetadataEntity,
partnerEntity,
localAssetUploadEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
@@ -108,7 +113,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
personEntity,
assetFaceEntity,
storeEntity,
i11.idxLatLng,
i12.idxLatLng,
];
@override
i0.StreamQueryUpdateRules
@@ -197,6 +202,15 @@ abstract class $Drift extends i0.GeneratedDatabase {
),
result: [i0.TableUpdate('partner_entity', kind: i0.UpdateKind.delete)],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'local_asset_entity',
limitUpdateKind: i0.UpdateKind.delete,
),
result: [
i0.TableUpdate('local_asset_upload_entity', kind: i0.UpdateKind.delete),
],
),
i0.WritePropagation(
on: i0.TableUpdateQuery.onTableName(
'remote_asset_entity',
@@ -317,23 +331,28 @@ class $DriftManager {
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(
i11.$$LocalAssetUploadEntityTableTableManager get localAssetUploadEntity =>
i11.$$LocalAssetUploadEntityTableTableManager(
_db,
_db.localAssetUploadEntity,
);
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);
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);
}

View File

@@ -5037,6 +5037,441 @@ final class Schema12 extends i0.VersionedSchema {
);
}
final class Schema13 extends i0.VersionedSchema {
Schema13({required super.database}) : super(version: 13);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
localAssetUploadEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
idxLatLng,
];
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 Shape23 localAssetUploadEntity = Shape23(
source: i0.VersionedTable(
entityName: 'local_asset_upload_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [_column_34, _column_95, _column_96, _column_97, _column_98],
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,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
}
class Shape23 extends i0.VersionedTable {
Shape23({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get assetId =>
columnsByName['asset_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get numberOfAttempts =>
columnsByName['number_of_attempts']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get lastAttemptAt =>
columnsByName['last_attempt_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get errorType =>
columnsByName['error_type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get errorMessage =>
columnsByName['error_message']! as i1.GeneratedColumn<String>;
}
i1.GeneratedColumn<int> _column_95(String aliasedName) =>
i1.GeneratedColumn<int>(
'number_of_attempts',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<DateTime> _column_96(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'last_attempt_at',
aliasedName,
false,
type: i1.DriftSqlType.dateTime,
defaultValue: const CustomExpression('CURRENT_TIMESTAMP'),
);
i1.GeneratedColumn<int> _column_97(String aliasedName) =>
i1.GeneratedColumn<int>(
'error_type',
aliasedName,
false,
type: i1.DriftSqlType.int,
defaultValue: const CustomExpression('0'),
);
i1.GeneratedColumn<String> _column_98(String aliasedName) =>
i1.GeneratedColumn<String>(
'error_message',
aliasedName,
true,
type: i1.DriftSqlType.string,
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -5049,6 +5484,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
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,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -5107,6 +5543,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from11To12(migrator, schema);
return 12;
case 12:
final schema = Schema13(database: database);
final migrator = i1.Migrator(database, schema);
await from12To13(migrator, schema);
return 13;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -5125,6 +5566,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
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,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -5138,5 +5580,6 @@ i1.OnUpgrade stepByStep({
from9To10: from9To10,
from10To11: from10To11,
from11To12: from11To12,
from12To13: from12To13,
),
);

View File

@@ -0,0 +1,70 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset_upload_entity.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
class DriftLocalAssetUploadRepository extends DriftDatabaseRepository {
final Drift _db;
const DriftLocalAssetUploadRepository(this._db) : super(_db);
Stream<List<DriftUploadStatus>> watchAll() {
final query = _db.localAssetUploadEntity.select().addColumns([_db.localAssetEntity.name]).join([
leftOuterJoin(
_db.localAssetEntity,
_db.localAssetEntity.id.equalsExp(_db.localAssetUploadEntity.assetId),
useColumns: false,
),
]);
return query.map((row) {
final upload = row.readTable(_db.localAssetUploadEntity);
final assetName = row.read(_db.localAssetEntity.name)!;
return DriftUploadStatus(taskId: upload.assetId, filename: assetName, error: upload.errorMessage, isFailed: true);
}).watch();
}
Future<void> upsert(String assetId, UploadErrorType errorType, String error) {
return _db
.into(_db.localAssetUploadEntity)
.insert(
LocalAssetUploadEntityCompanion(
assetId: Value(assetId),
numberOfAttempts: const Value(1),
lastAttemptAt: Value(DateTime.now()),
errorType: Value(errorType),
errorMessage: Value(error),
),
onConflict: DoUpdate(
(old) => LocalAssetUploadEntityCompanion.custom(
numberOfAttempts: (old.numberOfAttempts + const Constant(1)),
lastAttemptAt: currentDateAndTime,
errorType: Variable.withInt(errorType.index),
errorMessage: Variable.withString(error),
),
),
);
}
Future<void> delete(String assetId) async {
await _db.managers.localAssetUploadEntity.filter((row) => row.assetId.id.equals(assetId)).delete();
}
Future<void> prune() async {
final query =
_db.localAssetUploadEntity.selectOnly().join([
leftOuterJoin(
_db.localAssetEntity,
_db.localAssetUploadEntity.assetId.equalsExp(_db.localAssetEntity.id),
useColumns: false,
),
leftOuterJoin(
_db.remoteAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])
..where(_db.remoteAssetEntity.checksum.isNotNull())
..addColumns([_db.localAssetUploadEntity.assetId]);
await _db.localAssetUploadEntity.deleteWhere((row) => row.assetId.isInQuery(query));
}
}

View File

@@ -25,6 +25,7 @@ import 'package:immich_mobile/providers/asset_viewer/share_intent_upload.provide
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/infrastructure/upload.provider.dart';
import 'package:immich_mobile/providers/locale_provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/theme.provider.dart';
@@ -211,6 +212,8 @@ class ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserve
WidgetsBinding.instance.addPostFrameCallback((_) {
// needs to be delayed so that EasyLocalization is working
if (Store.isBetaTimelineEnabled) {
// Start upload timer
ref.read(uploadTimerProvider.notifier).start();
ref.read(backgroundServiceProvider).disableService();
ref.read(backgroundWorkerFgServiceProvider).enable();
if (Platform.isAndroid) {

View File

@@ -1,4 +1,5 @@
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -7,6 +8,8 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.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/upload.provider.dart';
import 'package:immich_mobile/services/upload.service.dart';
import 'package:immich_mobile/utils/bytes_units.dart';
import 'package:path/path.dart' as path;
@@ -16,7 +19,10 @@ class DriftUploadDetailPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final uploadItems = ref.watch(driftBackupProvider.select((state) => state.uploadItems));
final inProgressUploads = ref.watch(driftBackupProvider.select((state) => state.uploadItems));
final failedUploads = ref.watch(failedUploadStatusProvider).valueOrNull ?? {};
// In-progress uploads take precedence over failed uploads with the same key
final uploadItems = {...failedUploads, ...inProgressUploads};
return Scaffold(
appBar: AppBar(
@@ -46,21 +52,21 @@ class DriftUploadDetailPage extends ConsumerWidget {
}
Widget _buildUploadList(Map<String, DriftUploadStatus> uploadItems) {
final sortedKeys = uploadItems.keys.sorted();
return ListView.separated(
addAutomaticKeepAlives: true,
padding: const EdgeInsets.all(16),
itemCount: uploadItems.length,
itemCount: sortedKeys.length,
separatorBuilder: (context, index) => const SizedBox(height: 4),
itemBuilder: (context, index) {
final item = uploadItems.values.elementAt(index);
final item = uploadItems[sortedKeys[index]]!;
return _buildUploadCard(context, item);
},
);
}
Widget _buildUploadCard(BuildContext context, DriftUploadStatus item) {
final isCompleted = item.progress >= 1.0;
final double progressPercentage = (item.progress * 100).clamp(0, 100);
final progress = item.progress;
return Card(
elevation: 0,
@@ -108,13 +114,16 @@ class DriftUploadDetailPage extends ConsumerWidget {
],
),
),
_buildProgressIndicator(
context,
item.progress,
progressPercentage,
isCompleted,
item.networkSpeedAsString,
),
if (item.isFailed == true)
_buildRetryButton(item)
else if (progress != null)
_buildProgressIndicator(
context,
progress,
(progress * 100).clamp(0, 100),
progress >= 1.0,
item.networkSpeedAsString,
),
],
),
],
@@ -129,7 +138,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
double progress,
double percentage,
bool isCompleted,
String networkSpeedAsString,
String? networkSpeedAsString,
) {
return Column(
children: [
@@ -159,17 +168,38 @@ class DriftUploadDetailPage extends ConsumerWidget {
),
],
),
Text(
networkSpeedAsString,
style: context.textTheme.labelSmall?.copyWith(
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
if (networkSpeedAsString != null)
Text(
networkSpeedAsString,
style: context.textTheme.labelSmall?.copyWith(
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 10,
),
),
),
],
);
}
Widget _buildRetryButton(DriftUploadStatus item) {
return Consumer(
builder: (context, ref, child) {
return IconButton(
onPressed: () => _retryFailedUpload(ref, item),
icon: const Icon(Icons.refresh_rounded),
iconSize: 24,
color: context.colorScheme.onErrorContainer,
tooltip: "retry_upload".t(context: context),
);
},
);
}
Future<void> _retryFailedUpload(WidgetRef ref, DriftUploadStatus item) async {
await ref.read(uploadServiceProvider).clearError(item.taskId);
ref.invalidate(failedUploadStatusProvider);
await ref.read(uploadServiceProvider).manualBackupId(item.taskId);
}
Future<void> _showFileDetailDialog(BuildContext context, DriftUploadStatus item) {
return showDialog(
context: context,
@@ -241,7 +271,8 @@ class FileDetailDialog extends ConsumerWidget {
_buildInfoSection(context, [
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)),
_buildInfoRow(context, "Local ID", asset.id),
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)),
if (uploadStatus.fileSize != null)
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize!, 2)),
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"),
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"),
_buildInfoRow(context, "Created At", asset.createdAt.toString()),

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/providers/backup/ios_background_settings.provider.
import 'package:immich_mobile/providers/backup/manual_upload.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/infrastructure/upload.provider.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -139,6 +140,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
Future<void> _handleBetaTimelineResume() async {
_ref.read(backupProvider.notifier).cancelBackup();
_ref.read(uploadTimerProvider.notifier).start();
unawaited(_ref.read(backgroundWorkerLockServiceProvider).lock());
// Give isolates time to complete any ongoing database transactions
@@ -216,6 +218,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
try {
if (Store.isBetaTimelineEnabled) {
_ref.read(uploadTimerProvider.notifier).stop();
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
}
await _performPause();
@@ -250,6 +253,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
state = AppLifeCycleEnum.detached;
if (Store.isBetaTimelineEnabled) {
_ref.read(uploadTimerProvider.notifier).stop();
unawaited(_ref.read(backgroundWorkerLockServiceProvider).unlock());
}

View File

@@ -5,6 +5,7 @@ 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/constants/enums.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';
@@ -32,18 +33,18 @@ class EnqueueStatus {
class DriftUploadStatus {
final String taskId;
final String filename;
final double progress;
final int fileSize;
final String networkSpeedAsString;
final double? progress;
final int? fileSize;
final String? networkSpeedAsString;
final bool? isFailed;
final String? error;
const DriftUploadStatus({
required this.taskId,
required this.filename,
required this.progress,
required this.fileSize,
required this.networkSpeedAsString,
this.progress,
this.fileSize,
this.networkSpeedAsString,
this.isFailed,
this.error,
});
@@ -231,7 +232,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
}
}
void _handleTaskStatusUpdate(TaskStatusUpdate update) {
void _handleTaskStatusUpdate(TaskStatusUpdate update) async {
final taskId = update.task.taskId;
switch (update.status) {
@@ -249,35 +250,36 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
});
}
await _uploadService.clearError(taskId);
case TaskStatus.failed:
_removeUploadItem(taskId);
// Ignore retry errors to avoid confusing users
if (update.exception?.description == 'Delayed or retried enqueue failed') {
_removeUploadItem(taskId);
return;
}
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();
if (exception != null) {
final errorType = switch (exception) {
TaskConnectionException() => UploadErrorType.network,
TaskFileSystemException() || TaskUrlException() => UploadErrorType.client,
TaskHttpException() => UploadErrorType.server,
_ => UploadErrorType.unknown,
};
state = state.copyWith(
uploadItems: {
...state.uploadItems,
taskId: currentItem.copyWith(isFailed: true, error: error),
},
);
if (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 ??= exception.toString();
await _uploadService.updateError(taskId, errorType, error);
}
_logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
break;
@@ -350,9 +352,9 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
state = state.copyWith(isSyncing: isSyncing);
}
Future<void> startBackup(String userId) {
Future<void> startBackup(String userId, {bool ignoreFailed = false}) {
state = state.copyWith(error: BackupError.none);
return _uploadService.startBackup(userId, _updateEnqueueCount);
return _uploadService.startBackup(userId, _updateEnqueueCount, ignoreFailed: ignoreFailed);
}
void _updateEnqueueCount(EnqueueStatus status) {

View File

@@ -0,0 +1,91 @@
import 'dart:async';
import 'package:background_downloader/background_downloader.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset_upload.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/db.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
class UploadTimerNotifier extends Notifier<bool> {
Timer? _timer;
final _timerLogger = Logger('UploadTimer');
static const _refreshDuration = Duration(seconds: 45);
void start() {
if (state) {
return;
}
state = true;
_schedule();
}
void stop() {
if (!state) {
return;
}
_timer?.cancel();
_timer = null;
state = false;
}
void _schedule() {
_timer?.cancel();
_timer = Timer(_refreshDuration, () async {
if (!state) {
return;
}
await _backup();
if (state) {
_schedule();
}
});
}
Future<void> _backup() async {
final isBackupEnabled = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.enableBackup);
if (!isBackupEnabled) {
_timerLogger.fine("UploadTimer: Backup is disabled, skipping backup start.");
return;
}
final tasks = await FileDownloader().allTasks(group: kBackupGroup);
final currentUserId = ref.read(currentUserProvider)?.id;
if (tasks.isEmpty && currentUserId != null) {
await ref.read(driftBackupProvider.notifier).startBackup(currentUserId, ignoreFailed: true);
} else {
_timerLogger.fine("UploadTimer: There are still active upload tasks - ${tasks.length}, skipping backup start.");
}
}
@override
bool build() {
Future.microtask(start);
ref.onDispose(() {
_timer?.cancel();
});
// Timer is not running yet
return false;
}
}
final uploadTimerProvider = NotifierProvider<UploadTimerNotifier, bool>(UploadTimerNotifier.new);
final assetUploadRepositoryProvider = Provider<DriftLocalAssetUploadRepository>(
(ref) => DriftLocalAssetUploadRepository(ref.watch(driftProvider)),
);
final failedUploadStatusProvider = StreamProvider.autoDispose<Map<String, DriftUploadStatus>>((ref) {
return ref.watch(assetUploadRepositoryProvider).watchAll().map((uploads) {
return uploads.fold<Map<String, DriftUploadStatus>>({}, (acc, upload) {
acc[upload.taskId] = upload;
return acc;
});
});
});

View File

@@ -7,17 +7,20 @@ 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/constants/enums.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/local_asset_upload.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/providers/infrastructure/upload.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';
@@ -34,6 +37,7 @@ final uploadServiceProvider = Provider((ref) {
ref.watch(localAssetRepository),
ref.watch(appSettingsServiceProvider),
ref.watch(assetMediaRepositoryProvider),
ref.watch(assetUploadRepositoryProvider),
);
ref.onDispose(service.dispose);
@@ -48,6 +52,7 @@ class UploadService {
this._localAssetRepository,
this._appSettingsService,
this._assetMediaRepository,
this._assetUploadRepository,
) {
_uploadRepository.onUploadStatus = _onUploadCallback;
_uploadRepository.onTaskProgress = _onTaskProgressCallback;
@@ -59,6 +64,7 @@ class UploadService {
final DriftLocalAssetRepository _localAssetRepository;
final AppSettingsService _appSettingsService;
final AssetMediaRepository _assetMediaRepository;
final DriftLocalAssetUploadRepository _assetUploadRepository;
final Logger _logger = Logger('UploadService');
final StreamController<TaskStatusUpdate> _taskStatusController = StreamController<TaskStatusUpdate>.broadcast();
@@ -87,6 +93,14 @@ class UploadService {
_taskProgressController.close();
}
Future<void> updateError(String assetId, UploadErrorType errorType, String error) {
return _assetUploadRepository.upsert(assetId, errorType, error);
}
Future<void> clearError(String assetId) {
return _assetUploadRepository.delete(assetId);
}
Future<List<bool>> enqueueTasks(List<UploadTask> tasks) {
return _uploadRepository.enqueueBackgroundAll(tasks);
}
@@ -99,6 +113,15 @@ class UploadService {
return _backupRepository.getAllCounts(userId);
}
Future<void> manualBackupId(String localId) async {
final localAsset = await _localAssetRepository.get(localId);
if (localAsset == null) {
_logger.warning('Local asset with id $localId not found for manual backup');
return;
}
await manualBackup([localAsset]);
}
Future<void> manualBackup(List<LocalAsset> localAssets) async {
await _storageRepository.clearCache();
List<UploadTask> tasks = [];
@@ -121,47 +144,41 @@ class UploadService {
/// Find backup candidates
/// Build the upload tasks
/// Enqueue the tasks
Future<void> startBackup(String userId, void Function(EnqueueStatus status) onEnqueueTasks) async {
Future<void> startBackup(
String userId,
void Function(EnqueueStatus status) onEnqueueTasks, {
bool ignoreFailed = false,
}) async {
await _storageRepository.clearCache();
await _assetUploadRepository.prune();
shouldAbortQueuingTasks = false;
final candidates = await _backupRepository.getCandidates(userId);
final candidates = await _backupRepository.getCandidates(
userId,
limit: 100,
ignoreFailed: ignoreFailed,
sortBy: SortCandidatesBy.attemptCount,
);
if (candidates.isEmpty) {
return;
}
const batchSize = 100;
int count = 0;
for (int i = 0; i < candidates.length; i += batchSize) {
if (shouldAbortQueuingTasks) {
break;
}
final tasks = (await Future.wait(candidates.map((asset) => getUploadTask(asset)))).nonNulls.toList();
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
await enqueueTasks(tasks);
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));
}
onEnqueueTasks(EnqueueStatus(enqueueCount: tasks.length, totalCount: candidates.length));
}
}
Future<void> startBackupWithHttpClient(String userId, bool hasWifi, CancellationToken token) async {
await _storageRepository.clearCache();
await _assetUploadRepository.prune();
shouldAbortQueuingTasks = false;
final candidates = await _backupRepository.getCandidates(userId);
final candidates = await _backupRepository.getCandidates(userId, sortBy: SortCandidatesBy.attemptCount);
if (candidates.isEmpty) {
return;
}
@@ -200,6 +217,7 @@ class UploadService {
shouldAbortQueuingTasks = true;
await _storageRepository.clearCache();
await _assetUploadRepository.prune();
await _uploadRepository.reset(kBackupGroup);
await _uploadRepository.deleteDatabaseRecords(kBackupGroup);

View File

@@ -15,6 +15,7 @@ import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10;
import 'schema_v11.dart' as v11;
import 'schema_v12.dart' as v12;
import 'schema_v13.dart' as v13;
class GeneratedHelper implements SchemaInstantiationHelper {
@override
@@ -44,10 +45,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v11.DatabaseAtV11(db);
case 12:
return v12.DatabaseAtV12(db);
case 13:
return v13.DatabaseAtV13(db);
default:
throw MissingSchemaException(version, versions);
}
}
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,19 @@
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset_upload.repository.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/auth.repository.dart';
import 'package:immich_mobile/repositories/auth_api.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/etag.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/repositories/local_files_manager.repository.dart';
import 'package:immich_mobile/repositories/partner.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:mocktail/mocktail.dart';
class MockAlbumRepository extends Mock implements AlbumRepository {}
@@ -46,3 +47,5 @@ class MockPartnerRepository extends Mock implements PartnerRepository {}
class MockPartnerApiRepository extends Mock implements PartnerApiRepository {}
class MockLocalFilesManagerRepository extends Mock implements LocalFilesManagerRepository {}
class MockLocalAssetUploadRepository extends Mock implements DriftLocalAssetUploadRepository {}

View File

@@ -29,6 +29,7 @@ void main() {
late MockDriftLocalAssetRepository mockLocalAssetRepository;
late MockAppSettingsService mockAppSettingsService;
late MockAssetMediaRepository mockAssetMediaRepository;
late MockLocalAssetUploadRepository mockLocalAssetUploadRepository;
late Drift db;
setUpAll(() async {
@@ -53,6 +54,7 @@ void main() {
mockLocalAssetRepository = MockDriftLocalAssetRepository();
mockAppSettingsService = MockAppSettingsService();
mockAssetMediaRepository = MockAssetMediaRepository();
mockLocalAssetUploadRepository = MockLocalAssetUploadRepository();
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadVideos)).thenReturn(false);
when(() => mockAppSettingsService.getSetting(AppSettingsEnum.useCellularForUploadPhotos)).thenReturn(false);
@@ -64,6 +66,7 @@ void main() {
mockLocalAssetRepository,
mockAppSettingsService,
mockAssetMediaRepository,
mockLocalAssetUploadRepository,
);
mockUploadRepository.onUploadStatus = (_) {};