Compare commits
1 Commits
feat/panor
...
postgres-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbdac440fd |
@@ -11,6 +11,7 @@ services:
|
||||
immich-server:
|
||||
container_name: immich-e2e-server
|
||||
image: immich-server:latest
|
||||
shm_size: 128mb
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: server/Dockerfile
|
||||
|
||||
1
mobile/drift_schemas/main/drift_schema_v21.json
generated
@@ -11,10 +11,6 @@ enum AssetType {
|
||||
|
||||
enum AssetState { local, remote, merged }
|
||||
|
||||
// do not change!
|
||||
// keep in sync with PlatformAssetPlaybackStyle
|
||||
enum AssetPlaybackStyle { unknown, image, video, imageAnimated, livePhoto, videoLooping }
|
||||
|
||||
sealed class BaseAsset {
|
||||
final String name;
|
||||
final String? checksum;
|
||||
@@ -47,14 +43,6 @@ sealed class BaseAsset {
|
||||
|
||||
bool get isMotionPhoto => livePhotoVideoId != null;
|
||||
|
||||
AssetPlaybackStyle get playbackStyle {
|
||||
if (isVideo) return AssetPlaybackStyle.video;
|
||||
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
|
||||
if (isImage && durationInSeconds != null && durationInSeconds! > 0) return AssetPlaybackStyle.imageAnimated;
|
||||
if (isImage) return AssetPlaybackStyle.image;
|
||||
return AssetPlaybackStyle.unknown;
|
||||
}
|
||||
|
||||
Duration get duration {
|
||||
final durationInSeconds = this.durationInSeconds;
|
||||
if (durationInSeconds != null) {
|
||||
|
||||
@@ -5,8 +5,6 @@ class LocalAsset extends BaseAsset {
|
||||
final String? remoteAssetId;
|
||||
final String? cloudId;
|
||||
final int orientation;
|
||||
@override
|
||||
final AssetPlaybackStyle playbackStyle;
|
||||
|
||||
final DateTime? adjustmentTime;
|
||||
final double? latitude;
|
||||
@@ -27,7 +25,6 @@ class LocalAsset extends BaseAsset {
|
||||
super.isFavorite = false,
|
||||
super.livePhotoVideoId,
|
||||
this.orientation = 0,
|
||||
required this.playbackStyle,
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
@@ -59,7 +56,6 @@ class LocalAsset extends BaseAsset {
|
||||
width: ${width ?? "<NA>"},
|
||||
height: ${height ?? "<NA>"},
|
||||
durationInSeconds: ${durationInSeconds ?? "<NA>"},
|
||||
playbackStyle: $playbackStyle,
|
||||
remoteId: ${remoteId ?? "<NA>"},
|
||||
cloudId: ${cloudId ?? "<NA>"},
|
||||
checksum: ${checksum ?? "<NA>"},
|
||||
@@ -80,7 +76,6 @@ class LocalAsset extends BaseAsset {
|
||||
id == other.id &&
|
||||
cloudId == other.cloudId &&
|
||||
orientation == other.orientation &&
|
||||
playbackStyle == other.playbackStyle &&
|
||||
adjustmentTime == other.adjustmentTime &&
|
||||
latitude == other.latitude &&
|
||||
longitude == other.longitude;
|
||||
@@ -92,7 +87,6 @@ class LocalAsset extends BaseAsset {
|
||||
id.hashCode ^
|
||||
remoteId.hashCode ^
|
||||
orientation.hashCode ^
|
||||
playbackStyle.hashCode ^
|
||||
adjustmentTime.hashCode ^
|
||||
latitude.hashCode ^
|
||||
longitude.hashCode;
|
||||
@@ -111,7 +105,6 @@ class LocalAsset extends BaseAsset {
|
||||
int? durationInSeconds,
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
AssetPlaybackStyle? playbackStyle,
|
||||
DateTime? adjustmentTime,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
@@ -131,7 +124,6 @@ class LocalAsset extends BaseAsset {
|
||||
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
playbackStyle: playbackStyle ?? this.playbackStyle,
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
|
||||
@@ -435,19 +435,9 @@ extension PlatformToLocalAsset on PlatformAsset {
|
||||
durationInSeconds: durationInSeconds,
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
playbackStyle: _toPlaybackStyle(playbackStyle),
|
||||
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) {
|
||||
PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown,
|
||||
PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image,
|
||||
PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video,
|
||||
PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated,
|
||||
PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto,
|
||||
PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping,
|
||||
};
|
||||
|
||||
@@ -25,8 +25,6 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
|
||||
|
||||
RealColumn get longitude => real().nullable()();
|
||||
|
||||
IntColumn get playbackStyle => intEnum<AssetPlaybackStyle>().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -45,7 +43,6 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
|
||||
width: width,
|
||||
remoteId: remoteId,
|
||||
orientation: orientation,
|
||||
playbackStyle: playbackStyle,
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
|
||||
@@ -25,7 +25,6 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
|
||||
i0.Value<DateTime?> adjustmentTime,
|
||||
i0.Value<double?> latitude,
|
||||
i0.Value<double?> longitude,
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
|
||||
});
|
||||
typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i1.LocalAssetEntityCompanion Function({
|
||||
@@ -44,7 +43,6 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<DateTime?> adjustmentTime,
|
||||
i0.Value<double?> latitude,
|
||||
i0.Value<double?> longitude,
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
|
||||
});
|
||||
|
||||
class $$LocalAssetEntityTableFilterComposer
|
||||
@@ -131,16 +129,6 @@ class $$LocalAssetEntityTableFilterComposer
|
||||
column: $table.longitude,
|
||||
builder: (column) => i0.ColumnFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<
|
||||
i2.AssetPlaybackStyle,
|
||||
i2.AssetPlaybackStyle,
|
||||
int
|
||||
>
|
||||
get playbackStyle => $composableBuilder(
|
||||
column: $table.playbackStyle,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$LocalAssetEntityTableOrderingComposer
|
||||
@@ -226,11 +214,6 @@ class $$LocalAssetEntityTableOrderingComposer
|
||||
column: $table.longitude,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get playbackStyle => $composableBuilder(
|
||||
column: $table.playbackStyle,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$LocalAssetEntityTableAnnotationComposer
|
||||
@@ -294,12 +277,6 @@ class $$LocalAssetEntityTableAnnotationComposer
|
||||
|
||||
i0.GeneratedColumn<double> get longitude =>
|
||||
$composableBuilder(column: $table.longitude, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
|
||||
get playbackStyle => $composableBuilder(
|
||||
column: $table.playbackStyle,
|
||||
builder: (column) => column,
|
||||
);
|
||||
}
|
||||
|
||||
class $$LocalAssetEntityTableTableManager
|
||||
@@ -357,8 +334,6 @@ class $$LocalAssetEntityTableTableManager
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
|
||||
const i0.Value.absent(),
|
||||
}) => i1.LocalAssetEntityCompanion(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -375,7 +350,6 @@ class $$LocalAssetEntityTableTableManager
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
playbackStyle: playbackStyle,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
@@ -394,8 +368,6 @@ class $$LocalAssetEntityTableTableManager
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
|
||||
const i0.Value.absent(),
|
||||
}) => i1.LocalAssetEntityCompanion.insert(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -412,7 +384,6 @@ class $$LocalAssetEntityTableTableManager
|
||||
adjustmentTime: adjustmentTime,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
playbackStyle: playbackStyle,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
@@ -625,19 +596,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
|
||||
playbackStyle =
|
||||
i0.GeneratedColumn<int>(
|
||||
'playback_style',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i4.Constant(0),
|
||||
).withConverter<i2.AssetPlaybackStyle>(
|
||||
i1.$LocalAssetEntityTable.$converterplaybackStyle,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
name,
|
||||
type,
|
||||
@@ -654,7 +612,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
playbackStyle,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -836,12 +793,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
i0.DriftSqlType.double,
|
||||
data['${effectivePrefix}longitude'],
|
||||
),
|
||||
playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}playback_style'],
|
||||
)!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -852,10 +803,6 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
|
||||
|
||||
static i0.JsonTypeConverter2<i2.AssetType, int, int> $convertertype =
|
||||
const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values);
|
||||
static i0.JsonTypeConverter2<i2.AssetPlaybackStyle, int, int>
|
||||
$converterplaybackStyle = const i0.EnumIndexConverter<i2.AssetPlaybackStyle>(
|
||||
i2.AssetPlaybackStyle.values,
|
||||
);
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
@@ -879,7 +826,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
final DateTime? adjustmentTime;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final i2.AssetPlaybackStyle playbackStyle;
|
||||
const LocalAssetEntityData({
|
||||
required this.name,
|
||||
required this.type,
|
||||
@@ -896,7 +842,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
this.adjustmentTime,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.playbackStyle,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -936,11 +881,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
if (!nullToAbsent || longitude != null) {
|
||||
map['longitude'] = i0.Variable<double>(longitude);
|
||||
}
|
||||
{
|
||||
map['playback_style'] = i0.Variable<int>(
|
||||
i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql(playbackStyle),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -967,9 +907,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
adjustmentTime: serializer.fromJson<DateTime?>(json['adjustmentTime']),
|
||||
latitude: serializer.fromJson<double?>(json['latitude']),
|
||||
longitude: serializer.fromJson<double?>(json['longitude']),
|
||||
playbackStyle: i1.$LocalAssetEntityTable.$converterplaybackStyle.fromJson(
|
||||
serializer.fromJson<int>(json['playbackStyle']),
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -993,9 +930,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
'adjustmentTime': serializer.toJson<DateTime?>(adjustmentTime),
|
||||
'latitude': serializer.toJson<double?>(latitude),
|
||||
'longitude': serializer.toJson<double?>(longitude),
|
||||
'playbackStyle': serializer.toJson<int>(
|
||||
i1.$LocalAssetEntityTable.$converterplaybackStyle.toJson(playbackStyle),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1015,7 +949,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
|
||||
i0.Value<double?> latitude = const i0.Value.absent(),
|
||||
i0.Value<double?> longitude = const i0.Value.absent(),
|
||||
i2.AssetPlaybackStyle? playbackStyle,
|
||||
}) => i1.LocalAssetEntityData(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
@@ -1036,7 +969,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
: this.adjustmentTime,
|
||||
latitude: latitude.present ? latitude.value : this.latitude,
|
||||
longitude: longitude.present ? longitude.value : this.longitude,
|
||||
playbackStyle: playbackStyle ?? this.playbackStyle,
|
||||
);
|
||||
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
|
||||
return LocalAssetEntityData(
|
||||
@@ -1063,9 +995,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
: this.adjustmentTime,
|
||||
latitude: data.latitude.present ? data.latitude.value : this.latitude,
|
||||
longitude: data.longitude.present ? data.longitude.value : this.longitude,
|
||||
playbackStyle: data.playbackStyle.present
|
||||
? data.playbackStyle.value
|
||||
: this.playbackStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1086,8 +1015,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
..write('iCloudId: $iCloudId, ')
|
||||
..write('adjustmentTime: $adjustmentTime, ')
|
||||
..write('latitude: $latitude, ')
|
||||
..write('longitude: $longitude, ')
|
||||
..write('playbackStyle: $playbackStyle')
|
||||
..write('longitude: $longitude')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -1109,7 +1037,6 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
adjustmentTime,
|
||||
latitude,
|
||||
longitude,
|
||||
playbackStyle,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -1129,8 +1056,7 @@ class LocalAssetEntityData extends i0.DataClass
|
||||
other.iCloudId == this.iCloudId &&
|
||||
other.adjustmentTime == this.adjustmentTime &&
|
||||
other.latitude == this.latitude &&
|
||||
other.longitude == this.longitude &&
|
||||
other.playbackStyle == this.playbackStyle);
|
||||
other.longitude == this.longitude);
|
||||
}
|
||||
|
||||
class LocalAssetEntityCompanion
|
||||
@@ -1150,7 +1076,6 @@ class LocalAssetEntityCompanion
|
||||
final i0.Value<DateTime?> adjustmentTime;
|
||||
final i0.Value<double?> latitude;
|
||||
final i0.Value<double?> longitude;
|
||||
final i0.Value<i2.AssetPlaybackStyle> playbackStyle;
|
||||
const LocalAssetEntityCompanion({
|
||||
this.name = const i0.Value.absent(),
|
||||
this.type = const i0.Value.absent(),
|
||||
@@ -1167,7 +1092,6 @@ class LocalAssetEntityCompanion
|
||||
this.adjustmentTime = const i0.Value.absent(),
|
||||
this.latitude = const i0.Value.absent(),
|
||||
this.longitude = const i0.Value.absent(),
|
||||
this.playbackStyle = const i0.Value.absent(),
|
||||
});
|
||||
LocalAssetEntityCompanion.insert({
|
||||
required String name,
|
||||
@@ -1185,7 +1109,6 @@ class LocalAssetEntityCompanion
|
||||
this.adjustmentTime = const i0.Value.absent(),
|
||||
this.latitude = const i0.Value.absent(),
|
||||
this.longitude = const i0.Value.absent(),
|
||||
this.playbackStyle = const i0.Value.absent(),
|
||||
}) : name = i0.Value(name),
|
||||
type = i0.Value(type),
|
||||
id = i0.Value(id);
|
||||
@@ -1205,7 +1128,6 @@ class LocalAssetEntityCompanion
|
||||
i0.Expression<DateTime>? adjustmentTime,
|
||||
i0.Expression<double>? latitude,
|
||||
i0.Expression<double>? longitude,
|
||||
i0.Expression<int>? playbackStyle,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (name != null) 'name': name,
|
||||
@@ -1223,7 +1145,6 @@ class LocalAssetEntityCompanion
|
||||
if (adjustmentTime != null) 'adjustment_time': adjustmentTime,
|
||||
if (latitude != null) 'latitude': latitude,
|
||||
if (longitude != null) 'longitude': longitude,
|
||||
if (playbackStyle != null) 'playback_style': playbackStyle,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1243,7 +1164,6 @@ class LocalAssetEntityCompanion
|
||||
i0.Value<DateTime?>? adjustmentTime,
|
||||
i0.Value<double?>? latitude,
|
||||
i0.Value<double?>? longitude,
|
||||
i0.Value<i2.AssetPlaybackStyle>? playbackStyle,
|
||||
}) {
|
||||
return i1.LocalAssetEntityCompanion(
|
||||
name: name ?? this.name,
|
||||
@@ -1261,7 +1181,6 @@ class LocalAssetEntityCompanion
|
||||
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
playbackStyle: playbackStyle ?? this.playbackStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1315,13 +1234,6 @@ class LocalAssetEntityCompanion
|
||||
if (longitude.present) {
|
||||
map['longitude'] = i0.Variable<double>(longitude.value);
|
||||
}
|
||||
if (playbackStyle.present) {
|
||||
map['playback_style'] = i0.Variable<int>(
|
||||
i1.$LocalAssetEntityTable.$converterplaybackStyle.toSql(
|
||||
playbackStyle.value,
|
||||
),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1342,8 +1254,7 @@ class LocalAssetEntityCompanion
|
||||
..write('iCloudId: $iCloudId, ')
|
||||
..write('adjustmentTime: $adjustmentTime, ')
|
||||
..write('latitude: $latitude, ')
|
||||
..write('longitude: $longitude, ')
|
||||
..write('playbackStyle: $playbackStyle')
|
||||
..write('longitude: $longitude')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ SELECT
|
||||
NULL as latitude,
|
||||
NULL as longitude,
|
||||
NULL as adjustmentTime,
|
||||
rae.is_edited,
|
||||
0 as playback_style
|
||||
rae.is_edited
|
||||
FROM
|
||||
remote_asset_entity rae
|
||||
LEFT JOIN
|
||||
@@ -64,8 +63,7 @@ SELECT
|
||||
lae.latitude,
|
||||
lae.longitude,
|
||||
lae.adjustment_time,
|
||||
0 as is_edited,
|
||||
lae.playback_style
|
||||
0 as is_edited
|
||||
FROM
|
||||
local_asset_entity lae
|
||||
WHERE NOT EXISTS (
|
||||
|
||||
@@ -29,7 +29,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
);
|
||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||
return customSelect(
|
||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited, 0 AS playback_style FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited, lae.playback_style FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
'SELECT rae.id AS remote_id, (SELECT lae.id FROM local_asset_entity AS lae WHERE lae.checksum = rae.checksum LIMIT 1) AS local_id, rae.name, rae.type, rae.created_at AS created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id, rae.live_photo_video_id, 0 AS orientation, rae.stack_id, NULL AS i_cloud_id, NULL AS latitude, NULL AS longitude, NULL AS adjustmentTime, rae.is_edited FROM remote_asset_entity AS rae LEFT JOIN stack_entity AS se ON rae.stack_id = se.id WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandeduserIds) AND(rae.stack_id IS NULL OR rae.id = se.primary_asset_id)UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at AS created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id, NULL AS live_photo_video_id, lae.orientation, NULL AS stack_id, lae.i_cloud_id, lae.latitude, lae.longitude, lae.adjustment_time, 0 AS is_edited FROM local_asset_entity AS lae WHERE NOT EXISTS (SELECT 1 FROM remote_asset_entity AS rae WHERE rae.checksum = lae.checksum AND rae.owner_id IN ($expandeduserIds)) AND EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 0) AND NOT EXISTS (SELECT 1 FROM local_album_asset_entity AS laa INNER JOIN local_album_entity AS la ON laa.album_id = la.id WHERE laa.asset_id = lae.id AND la.backup_selection = 2) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||
variables: [
|
||||
for (var $ in userIds) i0.Variable<String>($),
|
||||
...generatedlimit.introducedVariables,
|
||||
@@ -67,7 +67,6 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
||||
longitude: row.readNullable<double>('longitude'),
|
||||
adjustmentTime: row.readNullable<DateTime>('adjustmentTime'),
|
||||
isEdited: row.read<bool>('is_edited'),
|
||||
playbackStyle: row.read<int>('playback_style'),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -140,7 +139,6 @@ class MergedAssetResult {
|
||||
final double? longitude;
|
||||
final DateTime? adjustmentTime;
|
||||
final bool isEdited;
|
||||
final int playbackStyle;
|
||||
MergedAssetResult({
|
||||
this.remoteId,
|
||||
this.localId,
|
||||
@@ -163,7 +161,6 @@ class MergedAssetResult {
|
||||
this.longitude,
|
||||
this.adjustmentTime,
|
||||
required this.isEdited,
|
||||
required this.playbackStyle,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,8 +28,6 @@ class TrashedLocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntity
|
||||
|
||||
IntColumn get source => intEnum<TrashOrigin>()();
|
||||
|
||||
IntColumn get playbackStyle => intEnum<AssetPlaybackStyle>().withDefault(const Constant(0))();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id, albumId};
|
||||
}
|
||||
@@ -47,7 +45,6 @@ extension TrashedLocalAssetEntityDataDomainExtension on TrashedLocalAssetEntityD
|
||||
height: height,
|
||||
width: width,
|
||||
orientation: orientation,
|
||||
playbackStyle: playbackStyle,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ typedef $$TrashedLocalAssetEntityTableCreateCompanionBuilder =
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
required i3.TrashOrigin source,
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
|
||||
});
|
||||
typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i1.TrashedLocalAssetEntityCompanion Function({
|
||||
@@ -40,7 +39,6 @@ typedef $$TrashedLocalAssetEntityTableUpdateCompanionBuilder =
|
||||
i0.Value<bool> isFavorite,
|
||||
i0.Value<int> orientation,
|
||||
i0.Value<i3.TrashOrigin> source,
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle,
|
||||
});
|
||||
|
||||
class $$TrashedLocalAssetEntityTableFilterComposer
|
||||
@@ -119,16 +117,6 @@ class $$TrashedLocalAssetEntityTableFilterComposer
|
||||
column: $table.source,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
i0.ColumnWithTypeConverterFilters<
|
||||
i2.AssetPlaybackStyle,
|
||||
i2.AssetPlaybackStyle,
|
||||
int
|
||||
>
|
||||
get playbackStyle => $composableBuilder(
|
||||
column: $table.playbackStyle,
|
||||
builder: (column) => i0.ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$TrashedLocalAssetEntityTableOrderingComposer
|
||||
@@ -205,11 +193,6 @@ class $$TrashedLocalAssetEntityTableOrderingComposer
|
||||
column: $table.source,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
|
||||
i0.ColumnOrderings<int> get playbackStyle => $composableBuilder(
|
||||
column: $table.playbackStyle,
|
||||
builder: (column) => i0.ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$TrashedLocalAssetEntityTableAnnotationComposer
|
||||
@@ -266,12 +249,6 @@ class $$TrashedLocalAssetEntityTableAnnotationComposer
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i3.TrashOrigin, int> get source =>
|
||||
$composableBuilder(column: $table.source, builder: (column) => column);
|
||||
|
||||
i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
|
||||
get playbackStyle => $composableBuilder(
|
||||
column: $table.playbackStyle,
|
||||
builder: (column) => column,
|
||||
);
|
||||
}
|
||||
|
||||
class $$TrashedLocalAssetEntityTableTableManager
|
||||
@@ -333,8 +310,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
i0.Value<i3.TrashOrigin> source = const i0.Value.absent(),
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
|
||||
const i0.Value.absent(),
|
||||
}) => i1.TrashedLocalAssetEntityCompanion(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -349,7 +324,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
source: source,
|
||||
playbackStyle: playbackStyle,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
@@ -366,8 +340,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
i0.Value<bool> isFavorite = const i0.Value.absent(),
|
||||
i0.Value<int> orientation = const i0.Value.absent(),
|
||||
required i3.TrashOrigin source,
|
||||
i0.Value<i2.AssetPlaybackStyle> playbackStyle =
|
||||
const i0.Value.absent(),
|
||||
}) => i1.TrashedLocalAssetEntityCompanion.insert(
|
||||
name: name,
|
||||
type: type,
|
||||
@@ -382,7 +354,6 @@ class $$TrashedLocalAssetEntityTableTableManager
|
||||
isFavorite: isFavorite,
|
||||
orientation: orientation,
|
||||
source: source,
|
||||
playbackStyle: playbackStyle,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
|
||||
@@ -579,19 +550,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource,
|
||||
);
|
||||
@override
|
||||
late final i0.GeneratedColumnWithTypeConverter<i2.AssetPlaybackStyle, int>
|
||||
playbackStyle =
|
||||
i0.GeneratedColumn<int>(
|
||||
'playback_style',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i0.DriftSqlType.int,
|
||||
requiredDuringInsert: false,
|
||||
defaultValue: const i4.Constant(0),
|
||||
).withConverter<i2.AssetPlaybackStyle>(
|
||||
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle,
|
||||
);
|
||||
@override
|
||||
List<i0.GeneratedColumn> get $columns => [
|
||||
name,
|
||||
type,
|
||||
@@ -606,7 +564,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
isFavorite,
|
||||
orientation,
|
||||
source,
|
||||
playbackStyle,
|
||||
];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@@ -763,13 +720,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
data['${effectivePrefix}source'],
|
||||
)!,
|
||||
),
|
||||
playbackStyle: i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle
|
||||
.fromSql(
|
||||
attachedDatabase.typeMapping.read(
|
||||
i0.DriftSqlType.int,
|
||||
data['${effectivePrefix}playback_style'],
|
||||
)!,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -782,10 +732,6 @@ class $TrashedLocalAssetEntityTable extends i3.TrashedLocalAssetEntity
|
||||
const i0.EnumIndexConverter<i2.AssetType>(i2.AssetType.values);
|
||||
static i0.JsonTypeConverter2<i3.TrashOrigin, int, int> $convertersource =
|
||||
const i0.EnumIndexConverter<i3.TrashOrigin>(i3.TrashOrigin.values);
|
||||
static i0.JsonTypeConverter2<i2.AssetPlaybackStyle, int, int>
|
||||
$converterplaybackStyle = const i0.EnumIndexConverter<i2.AssetPlaybackStyle>(
|
||||
i2.AssetPlaybackStyle.values,
|
||||
);
|
||||
@override
|
||||
bool get withoutRowId => true;
|
||||
@override
|
||||
@@ -807,7 +753,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
final bool isFavorite;
|
||||
final int orientation;
|
||||
final i3.TrashOrigin source;
|
||||
final i2.AssetPlaybackStyle playbackStyle;
|
||||
const TrashedLocalAssetEntityData({
|
||||
required this.name,
|
||||
required this.type,
|
||||
@@ -822,7 +767,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
required this.isFavorite,
|
||||
required this.orientation,
|
||||
required this.source,
|
||||
required this.playbackStyle,
|
||||
});
|
||||
@override
|
||||
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -856,13 +800,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source),
|
||||
);
|
||||
}
|
||||
{
|
||||
map['playback_style'] = i0.Variable<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toSql(
|
||||
playbackStyle,
|
||||
),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -889,8 +826,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
source: i1.$TrashedLocalAssetEntityTable.$convertersource.fromJson(
|
||||
serializer.fromJson<int>(json['source']),
|
||||
),
|
||||
playbackStyle: i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle
|
||||
.fromJson(serializer.fromJson<int>(json['playbackStyle'])),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -914,11 +849,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
'source': serializer.toJson<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource.toJson(source),
|
||||
),
|
||||
'playbackStyle': serializer.toJson<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toJson(
|
||||
playbackStyle,
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -936,7 +866,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
bool? isFavorite,
|
||||
int? orientation,
|
||||
i3.TrashOrigin? source,
|
||||
i2.AssetPlaybackStyle? playbackStyle,
|
||||
}) => i1.TrashedLocalAssetEntityData(
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
@@ -953,7 +882,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
source: source ?? this.source,
|
||||
playbackStyle: playbackStyle ?? this.playbackStyle,
|
||||
);
|
||||
TrashedLocalAssetEntityData copyWithCompanion(
|
||||
i1.TrashedLocalAssetEntityCompanion data,
|
||||
@@ -978,9 +906,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
? data.orientation.value
|
||||
: this.orientation,
|
||||
source: data.source.present ? data.source.value : this.source,
|
||||
playbackStyle: data.playbackStyle.present
|
||||
? data.playbackStyle.value
|
||||
: this.playbackStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -999,8 +924,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation, ')
|
||||
..write('source: $source, ')
|
||||
..write('playbackStyle: $playbackStyle')
|
||||
..write('source: $source')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
@@ -1020,7 +944,6 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
isFavorite,
|
||||
orientation,
|
||||
source,
|
||||
playbackStyle,
|
||||
);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
@@ -1038,8 +961,7 @@ class TrashedLocalAssetEntityData extends i0.DataClass
|
||||
other.checksum == this.checksum &&
|
||||
other.isFavorite == this.isFavorite &&
|
||||
other.orientation == this.orientation &&
|
||||
other.source == this.source &&
|
||||
other.playbackStyle == this.playbackStyle);
|
||||
other.source == this.source);
|
||||
}
|
||||
|
||||
class TrashedLocalAssetEntityCompanion
|
||||
@@ -1057,7 +979,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
final i0.Value<bool> isFavorite;
|
||||
final i0.Value<int> orientation;
|
||||
final i0.Value<i3.TrashOrigin> source;
|
||||
final i0.Value<i2.AssetPlaybackStyle> playbackStyle;
|
||||
const TrashedLocalAssetEntityCompanion({
|
||||
this.name = const i0.Value.absent(),
|
||||
this.type = const i0.Value.absent(),
|
||||
@@ -1072,7 +993,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
this.source = const i0.Value.absent(),
|
||||
this.playbackStyle = const i0.Value.absent(),
|
||||
});
|
||||
TrashedLocalAssetEntityCompanion.insert({
|
||||
required String name,
|
||||
@@ -1088,7 +1008,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
this.isFavorite = const i0.Value.absent(),
|
||||
this.orientation = const i0.Value.absent(),
|
||||
required i3.TrashOrigin source,
|
||||
this.playbackStyle = const i0.Value.absent(),
|
||||
}) : name = i0.Value(name),
|
||||
type = i0.Value(type),
|
||||
id = i0.Value(id),
|
||||
@@ -1108,7 +1027,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
i0.Expression<bool>? isFavorite,
|
||||
i0.Expression<int>? orientation,
|
||||
i0.Expression<int>? source,
|
||||
i0.Expression<int>? playbackStyle,
|
||||
}) {
|
||||
return i0.RawValuesInsertable({
|
||||
if (name != null) 'name': name,
|
||||
@@ -1124,7 +1042,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
if (isFavorite != null) 'is_favorite': isFavorite,
|
||||
if (orientation != null) 'orientation': orientation,
|
||||
if (source != null) 'source': source,
|
||||
if (playbackStyle != null) 'playback_style': playbackStyle,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1142,7 +1059,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
i0.Value<bool>? isFavorite,
|
||||
i0.Value<int>? orientation,
|
||||
i0.Value<i3.TrashOrigin>? source,
|
||||
i0.Value<i2.AssetPlaybackStyle>? playbackStyle,
|
||||
}) {
|
||||
return i1.TrashedLocalAssetEntityCompanion(
|
||||
name: name ?? this.name,
|
||||
@@ -1158,7 +1074,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
orientation: orientation ?? this.orientation,
|
||||
source: source ?? this.source,
|
||||
playbackStyle: playbackStyle ?? this.playbackStyle,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1208,13 +1123,6 @@ class TrashedLocalAssetEntityCompanion
|
||||
i1.$TrashedLocalAssetEntityTable.$convertersource.toSql(source.value),
|
||||
);
|
||||
}
|
||||
if (playbackStyle.present) {
|
||||
map['playback_style'] = i0.Variable<int>(
|
||||
i1.$TrashedLocalAssetEntityTable.$converterplaybackStyle.toSql(
|
||||
playbackStyle.value,
|
||||
),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -1233,8 +1141,7 @@ class TrashedLocalAssetEntityCompanion
|
||||
..write('checksum: $checksum, ')
|
||||
..write('isFavorite: $isFavorite, ')
|
||||
..write('orientation: $orientation, ')
|
||||
..write('source: $source, ')
|
||||
..write('playbackStyle: $playbackStyle')
|
||||
..write('source: $source')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
}
|
||||
|
||||
@override
|
||||
int get schemaVersion => 21;
|
||||
int get schemaVersion => 20;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
@@ -230,10 +230,6 @@ class Drift extends $Drift implements IDatabaseRepository {
|
||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.isVisible);
|
||||
await m.addColumn(v20.assetFaceEntity, v20.assetFaceEntity.deletedAt);
|
||||
},
|
||||
from20To21: (m, v21) async {
|
||||
await m.addColumn(v21.localAssetEntity, v21.localAssetEntity.playbackStyle);
|
||||
await m.addColumn(v21.trashedLocalAssetEntity, v21.trashedLocalAssetEntity.playbackStyle);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -8904,591 +8904,6 @@ i1.GeneratedColumn<bool> _column_102(String aliasedName) =>
|
||||
),
|
||||
defaultValue: const CustomExpression('1'),
|
||||
);
|
||||
|
||||
final class Schema21 extends i0.VersionedSchema {
|
||||
Schema21({required super.database}) : super(version: 21);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
userEntity,
|
||||
remoteAssetEntity,
|
||||
stackEntity,
|
||||
localAssetEntity,
|
||||
remoteAlbumEntity,
|
||||
localAlbumEntity,
|
||||
localAlbumAssetEntity,
|
||||
idxLocalAlbumAssetAlbumAsset,
|
||||
idxRemoteAlbumOwnerId,
|
||||
idxLocalAssetChecksum,
|
||||
idxLocalAssetCloudId,
|
||||
idxStackPrimaryAssetId,
|
||||
idxRemoteAssetOwnerChecksum,
|
||||
uQRemoteAssetsOwnerChecksum,
|
||||
uQRemoteAssetsOwnerLibraryChecksum,
|
||||
idxRemoteAssetChecksum,
|
||||
idxRemoteAssetStackId,
|
||||
idxRemoteAssetLocalDateTimeDay,
|
||||
idxRemoteAssetLocalDateTimeMonth,
|
||||
authUserEntity,
|
||||
userMetadataEntity,
|
||||
partnerEntity,
|
||||
remoteExifEntity,
|
||||
remoteAlbumAssetEntity,
|
||||
remoteAlbumUserEntity,
|
||||
remoteAssetCloudIdEntity,
|
||||
memoryEntity,
|
||||
memoryAssetEntity,
|
||||
personEntity,
|
||||
assetFaceEntity,
|
||||
storeEntity,
|
||||
trashedLocalAssetEntity,
|
||||
idxPartnerSharedWithId,
|
||||
idxLatLng,
|
||||
idxRemoteAlbumAssetAlbumAsset,
|
||||
idxRemoteAssetCloudId,
|
||||
idxPersonOwnerId,
|
||||
idxAssetFacePersonId,
|
||||
idxAssetFaceAssetId,
|
||||
idxTrashedLocalAssetChecksum,
|
||||
idxTrashedLocalAssetAlbum,
|
||||
];
|
||||
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 Shape28 remoteAssetEntity = Shape28(
|
||||
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,
|
||||
_column_101,
|
||||
],
|
||||
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 Shape30 localAssetEntity = Shape30(
|
||||
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,
|
||||
_column_98,
|
||||
_column_96,
|
||||
_column_46,
|
||||
_column_47,
|
||||
_column_103,
|
||||
],
|
||||
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 idxLocalAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_local_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAlbumOwnerId = i1.Index(
|
||||
'idx_remote_album_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_owner_id ON remote_album_entity (owner_id)',
|
||||
);
|
||||
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 idxLocalAssetCloudId = i1.Index(
|
||||
'idx_local_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
|
||||
);
|
||||
final i1.Index idxStackPrimaryAssetId = i1.Index(
|
||||
'idx_stack_primary_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
|
||||
);
|
||||
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)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetStackId = i1.Index(
|
||||
'idx_remote_asset_stack_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
|
||||
'idx_remote_asset_local_date_time_day',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
|
||||
);
|
||||
final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
|
||||
'idx_remote_asset_local_date_time_month',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
|
||||
);
|
||||
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 Shape27 remoteAssetCloudIdEntity = Shape27(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'remote_asset_cloud_id_entity',
|
||||
withoutRowId: true,
|
||||
isStrict: true,
|
||||
tableConstraints: ['PRIMARY KEY(asset_id)'],
|
||||
columns: [
|
||||
_column_36,
|
||||
_column_99,
|
||||
_column_100,
|
||||
_column_96,
|
||||
_column_46,
|
||||
_column_47,
|
||||
],
|
||||
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 Shape29 assetFaceEntity = Shape29(
|
||||
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,
|
||||
_column_102,
|
||||
_column_18,
|
||||
],
|
||||
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 Shape31 trashedLocalAssetEntity = Shape31(
|
||||
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,
|
||||
_column_97,
|
||||
_column_103,
|
||||
],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxPartnerSharedWithId = i1.Index(
|
||||
'idx_partner_shared_with_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
|
||||
);
|
||||
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 idxRemoteAlbumAssetAlbumAsset = i1.Index(
|
||||
'idx_remote_album_asset_album_asset',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
|
||||
);
|
||||
final i1.Index idxRemoteAssetCloudId = i1.Index(
|
||||
'idx_remote_asset_cloud_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
|
||||
);
|
||||
final i1.Index idxPersonOwnerId = i1.Index(
|
||||
'idx_person_owner_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
|
||||
);
|
||||
final i1.Index idxAssetFacePersonId = i1.Index(
|
||||
'idx_asset_face_person_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
|
||||
);
|
||||
final i1.Index idxAssetFaceAssetId = i1.Index(
|
||||
'idx_asset_face_asset_id',
|
||||
'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
|
||||
);
|
||||
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 Shape30 extends i0.VersionedTable {
|
||||
Shape30({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get width =>
|
||||
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get height =>
|
||||
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get checksum =>
|
||||
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isFavorite =>
|
||||
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<int> get orientation =>
|
||||
columnsByName['orientation']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get iCloudId =>
|
||||
columnsByName['i_cloud_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<DateTime> get adjustmentTime =>
|
||||
columnsByName['adjustment_time']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<double> get latitude =>
|
||||
columnsByName['latitude']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<double> get longitude =>
|
||||
columnsByName['longitude']! as i1.GeneratedColumn<double>;
|
||||
i1.GeneratedColumn<int> get playbackStyle =>
|
||||
columnsByName['playback_style']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_103(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'playback_style',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
defaultValue: const CustomExpression('0'),
|
||||
);
|
||||
|
||||
class Shape31 extends i0.VersionedTable {
|
||||
Shape31({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get name =>
|
||||
columnsByName['name']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get type =>
|
||||
columnsByName['type']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<DateTime> get createdAt =>
|
||||
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<DateTime> get updatedAt =>
|
||||
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
|
||||
i1.GeneratedColumn<int> get width =>
|
||||
columnsByName['width']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get height =>
|
||||
columnsByName['height']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get durationInSeconds =>
|
||||
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get id =>
|
||||
columnsByName['id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get albumId =>
|
||||
columnsByName['album_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get checksum =>
|
||||
columnsByName['checksum']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<bool> get isFavorite =>
|
||||
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
|
||||
i1.GeneratedColumn<int> get orientation =>
|
||||
columnsByName['orientation']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get source =>
|
||||
columnsByName['source']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get playbackStyle =>
|
||||
columnsByName['playback_style']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
@@ -9509,7 +8924,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
@@ -9608,11 +9022,6 @@ i0.MigrationStepWithVersion migrationSteps({
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from19To20(migrator, schema);
|
||||
return 20;
|
||||
case 20:
|
||||
final schema = Schema21(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from20To21(migrator, schema);
|
||||
return 21;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
@@ -9639,7 +9048,6 @@ i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema18 schema) from17To18,
|
||||
required Future<void> Function(i1.Migrator m, Schema19 schema) from18To19,
|
||||
required Future<void> Function(i1.Migrator m, Schema20 schema) from19To20,
|
||||
required Future<void> Function(i1.Migrator m, Schema21 schema) from20To21,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(
|
||||
from1To2: from1To2,
|
||||
@@ -9661,6 +9069,5 @@ i1.OnUpgrade stepByStep({
|
||||
from17To18: from17To18,
|
||||
from18To19: from18To19,
|
||||
from19To20: from19To20,
|
||||
from20To21: from20To21,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -301,7 +301,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
id: asset.id,
|
||||
orientation: Value(asset.orientation),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
playbackStyle: Value(asset.playbackStyle),
|
||||
latitude: Value(asset.latitude),
|
||||
longitude: Value(asset.longitude),
|
||||
adjustmentTime: Value(asset.adjustmentTime),
|
||||
@@ -334,7 +333,6 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
||||
checksum: const Value(null),
|
||||
orientation: Value(asset.orientation),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
playbackStyle: Value(asset.playbackStyle),
|
||||
);
|
||||
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
|
||||
_db.localAssetEntity,
|
||||
|
||||
@@ -101,7 +101,6 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
isFavorite: row.isFavorite,
|
||||
durationInSeconds: row.durationInSeconds,
|
||||
orientation: row.orientation,
|
||||
playbackStyle: AssetPlaybackStyle.values[row.playbackStyle],
|
||||
cloudId: row.iCloudId,
|
||||
latitude: row.latitude,
|
||||
longitude: row.longitude,
|
||||
|
||||
@@ -85,7 +85,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
durationInSeconds: Value(item.asset.durationInSeconds),
|
||||
isFavorite: Value(item.asset.isFavorite),
|
||||
orientation: Value(item.asset.orientation),
|
||||
playbackStyle: Value(item.asset.playbackStyle),
|
||||
source: TrashOrigin.localSync,
|
||||
);
|
||||
|
||||
@@ -148,7 +147,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
durationInSeconds: Value(asset.durationInSeconds),
|
||||
isFavorite: Value(asset.isFavorite),
|
||||
orientation: Value(asset.orientation),
|
||||
playbackStyle: Value(asset.playbackStyle),
|
||||
createdAt: Value(asset.createdAt),
|
||||
updatedAt: Value(asset.updatedAt),
|
||||
source: const Value(TrashOrigin.remoteSync),
|
||||
@@ -197,7 +195,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
checksum: Value(e.checksum),
|
||||
isFavorite: Value(e.isFavorite),
|
||||
orientation: Value(e.orientation),
|
||||
playbackStyle: Value(e.playbackStyle),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -248,7 +245,6 @@ class DriftTrashedLocalAssetRepository extends DriftDatabaseRepository {
|
||||
checksum: Value(e.asset.checksum),
|
||||
isFavorite: Value(e.asset.isFavorite),
|
||||
orientation: Value(e.asset.orientation),
|
||||
playbackStyle: Value(e.asset.playbackStyle),
|
||||
source: TrashOrigin.localUser,
|
||||
albumId: e.albumId,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -12,8 +11,7 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/generated/translations.g.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.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';
|
||||
@@ -131,16 +129,11 @@ class _BottomPanelState extends State<_BottomPanel> {
|
||||
return;
|
||||
}
|
||||
|
||||
final db = Drift();
|
||||
try {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
for (final suffix in ['', '-wal', '-shm']) {
|
||||
final file = File(path.join(dir.path, 'immich.sqlite$suffix'));
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
return;
|
||||
await db.reset();
|
||||
} finally {
|
||||
await db.close();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
|
||||
@@ -25,7 +25,6 @@ class FileMediaRepository {
|
||||
type: AssetType.image,
|
||||
createdAt: entity.createDateTime,
|
||||
updatedAt: entity.modifiedDateTime,
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'dart:io';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:drift/drift.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/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
@@ -18,7 +17,6 @@ import 'package:immich_mobile/infrastructure/entities/device_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
@@ -35,7 +33,7 @@ import 'package:isar/isar.dart';
|
||||
// ignore: import_rule_photo_manager
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
|
||||
const int targetVersion = 23;
|
||||
const int targetVersion = 22;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
final hasVersion = Store.tryGet(StoreKey.version) != null;
|
||||
@@ -101,10 +99,6 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
|
||||
}
|
||||
}
|
||||
|
||||
if (version < 23 && Store.isBetaTimelineEnabled) {
|
||||
await _populateLocalAssetPlaybackStyle(drift);
|
||||
}
|
||||
|
||||
if (version < 22 && !Store.isBetaTimelineEnabled) {
|
||||
await Store.put(StoreKey.needBetaMigration, true);
|
||||
}
|
||||
@@ -398,52 +392,6 @@ Future<void> migrateStoreToIsar(Isar db, Drift drift) async {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _populateLocalAssetPlaybackStyle(Drift db) async {
|
||||
try {
|
||||
final nativeApi = NativeSyncApi();
|
||||
|
||||
final albums = await nativeApi.getAlbums();
|
||||
for (final album in albums) {
|
||||
final assets = await nativeApi.getAssetsForAlbum(album.id);
|
||||
await db.batch((batch) {
|
||||
for (final asset in assets) {
|
||||
batch.update(
|
||||
db.localAssetEntity,
|
||||
LocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))),
|
||||
where: (t) => t.id.equals(asset.id),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final trashedAssetMap = await nativeApi.getTrashedAssets();
|
||||
for (final assets in trashedAssetMap.values) {
|
||||
await db.batch((batch) {
|
||||
for (final asset in assets) {
|
||||
batch.update(
|
||||
db.trashedLocalAssetEntity,
|
||||
TrashedLocalAssetEntityCompanion(playbackStyle: Value(_toPlaybackStyle(asset.playbackStyle))),
|
||||
where: (t) => t.id.equals(asset.id),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dPrint(() => "[MIGRATION] Successfully populated playbackStyle for local and trashed assets");
|
||||
} catch (error) {
|
||||
dPrint(() => "[MIGRATION] Error while populating playbackStyle: $error");
|
||||
}
|
||||
}
|
||||
|
||||
AssetPlaybackStyle _toPlaybackStyle(PlatformAssetPlaybackStyle style) => switch (style) {
|
||||
PlatformAssetPlaybackStyle.unknown => AssetPlaybackStyle.unknown,
|
||||
PlatformAssetPlaybackStyle.image => AssetPlaybackStyle.image,
|
||||
PlatformAssetPlaybackStyle.video => AssetPlaybackStyle.video,
|
||||
PlatformAssetPlaybackStyle.imageAnimated => AssetPlaybackStyle.imageAnimated,
|
||||
PlatformAssetPlaybackStyle.livePhoto => AssetPlaybackStyle.livePhoto,
|
||||
PlatformAssetPlaybackStyle.videoLooping => AssetPlaybackStyle.videoLooping,
|
||||
};
|
||||
|
||||
class _DeviceAsset {
|
||||
final String assetId;
|
||||
final List<int>? hash;
|
||||
|
||||
1
mobile/openapi/README.md
generated
@@ -121,7 +121,6 @@ Class | Method | HTTP request | Description
|
||||
*AssetsApi* | [**updateBulkAssetMetadata**](doc//AssetsApi.md#updatebulkassetmetadata) | **PUT** /assets/metadata | Upsert asset metadata
|
||||
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | Upload asset
|
||||
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | View asset thumbnail
|
||||
*AssetsApi* | [**viewAssetTile**](doc//AssetsApi.md#viewassettile) | **GET** /assets/{id}/tiles/{level}/{col}/{row} | View an asset tile
|
||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | Change password
|
||||
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | Change pin code
|
||||
*AuthenticationApi* | [**finishOAuth**](doc//AuthenticationApi.md#finishoauth) | **POST** /oauth/callback | Finish OAuth
|
||||
|
||||
87
mobile/openapi/lib/api/assets_api.dart
generated
@@ -1836,91 +1836,4 @@ class AssetsApi {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// View an asset tile
|
||||
///
|
||||
/// Download a specific tile from an image at the specified level - must currently be 0 - and position
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] col (required):
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [num] level (required):
|
||||
///
|
||||
/// * [num] row (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> viewAssetTileWithHttpInfo(num col, String id, num level, num row, { String? key, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/assets/{id}/tiles/{level}/{col}/{row}'
|
||||
.replaceAll('{col}', col.toString())
|
||||
.replaceAll('{id}', id)
|
||||
.replaceAll('{level}', level.toString())
|
||||
.replaceAll('{row}', row.toString());
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
if (slug != null) {
|
||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||
}
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// View an asset tile
|
||||
///
|
||||
/// Download a specific tile from an image at the specified level - must currently be 0 - and position
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] col (required):
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [num] level (required):
|
||||
///
|
||||
/// * [num] row (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<MultipartFile?> viewAssetTile(num col, String id, num level, num row, { String? key, String? slug, }) async {
|
||||
final response = await viewAssetTileWithHttpInfo(col, id, level, row, key: key, slug: slug, );
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
4
mobile/test/drift/main/generated/schema.dart
generated
@@ -23,7 +23,6 @@ import 'schema_v17.dart' as v17;
|
||||
import 'schema_v18.dart' as v18;
|
||||
import 'schema_v19.dart' as v19;
|
||||
import 'schema_v20.dart' as v20;
|
||||
import 'schema_v21.dart' as v21;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
@@ -69,8 +68,6 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
return v19.DatabaseAtV19(db);
|
||||
case 20:
|
||||
return v20.DatabaseAtV20(db);
|
||||
case 21:
|
||||
return v21.DatabaseAtV21(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
@@ -97,6 +94,5 @@ class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
];
|
||||
}
|
||||
|
||||
8545
mobile/test/drift/main/generated/schema_v21.dart
generated
2
mobile/test/fixtures/asset.stub.dart
vendored
@@ -64,7 +64,6 @@ abstract final class LocalAssetStub {
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2025),
|
||||
updatedAt: DateTime(2025, 2),
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
@@ -74,7 +73,6 @@ abstract final class LocalAssetStub {
|
||||
type: AssetType.image,
|
||||
createdAt: DateTime(2000),
|
||||
updatedAt: DateTime(20021),
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -194,7 +194,6 @@ void main() {
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
adjustmentTime: DateTime(2026, 1, 2),
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
@@ -244,7 +243,6 @@ void main() {
|
||||
cloudId: 'cloud-id-123',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
@@ -283,7 +281,6 @@ void main() {
|
||||
createdAt: DateTime(2025, 1, 1),
|
||||
updatedAt: DateTime(2025, 1, 2),
|
||||
cloudId: null, // No cloudId
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
@@ -326,7 +323,6 @@ void main() {
|
||||
cloudId: 'cloud-id-livephoto',
|
||||
latitude: 37.7749,
|
||||
longitude: -122.4194,
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
|
||||
|
||||
@@ -155,7 +155,6 @@ abstract final class TestUtils {
|
||||
width: width,
|
||||
height: height,
|
||||
orientation: orientation,
|
||||
playbackStyle: domain.AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ class MediumFactory {
|
||||
type: type ?? AssetType.image,
|
||||
createdAt: createdAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
||||
updatedAt: updatedAt ?? DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)),
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ LocalAsset createLocalAsset({
|
||||
createdAt: createdAt ?? DateTime.now(),
|
||||
updatedAt: updatedAt ?? DateTime.now(),
|
||||
isFavorite: isFavorite,
|
||||
playbackStyle: AssetPlaybackStyle.image,
|
||||
isEdited: false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4397,103 +4397,6 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/assets/{id}/tiles/{level}/{col}/{row}": {
|
||||
"get": {
|
||||
"description": "Download a specific tile from an image at the specified level - must currently be 0 - and position",
|
||||
"operationId": "viewAssetTile",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "col",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "level",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "row",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/octet-stream": {
|
||||
"schema": {
|
||||
"format": "binary",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "View an asset tile",
|
||||
"tags": [
|
||||
"Assets"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.7.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.7.0",
|
||||
"state": "Stable"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "asset.view",
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/assets/{id}/video/playback": {
|
||||
"get": {
|
||||
"description": "Streams the video file for the specified asset. This endpoint also supports byte range requests.",
|
||||
|
||||
@@ -4313,27 +4313,6 @@ export function viewAsset({ edited, id, key, size, slug }: {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* View an asset tile
|
||||
*/
|
||||
export function viewAssetTile({ col, id, key, level, row, slug }: {
|
||||
col: number;
|
||||
id: string;
|
||||
key?: string;
|
||||
level: number;
|
||||
row: number;
|
||||
slug?: string;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||
status: 200;
|
||||
data: Blob;
|
||||
}>(`/assets/${encodeURIComponent(id)}/tiles/${encodeURIComponent(level)}/${encodeURIComponent(col)}/${encodeURIComponent(row)}${QS.query(QS.explode({
|
||||
key,
|
||||
slug
|
||||
}))}`, {
|
||||
...opts
|
||||
}));
|
||||
}
|
||||
/**
|
||||
* Play asset video
|
||||
*/
|
||||
|
||||
@@ -55,13 +55,6 @@ export const getAssetThumbnailPath = (id: string) => `/assets/${id}/thumbnail`;
|
||||
export const getAssetPlaybackPath = (id: string) =>
|
||||
`/assets/${id}/video/playback`;
|
||||
|
||||
export const getAssetTilePath = (
|
||||
id: string,
|
||||
level: number,
|
||||
col: number,
|
||||
row: number
|
||||
) => `/assets/${id}/tiles/${level}/${col}/${row}`;
|
||||
|
||||
export const getUserProfileImagePath = (userId: string) =>
|
||||
`/users/${userId}/profile-image`;
|
||||
|
||||
|
||||
204
pnpm-lock.yaml
generated
@@ -67,7 +67,7 @@ importers:
|
||||
version: 24.11.0
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
byte-size:
|
||||
specifier: ^9.0.0
|
||||
version: 9.0.1
|
||||
@@ -115,10 +115,10 @@ importers:
|
||||
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest-fetch-mock:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
yaml:
|
||||
specifier: ^2.3.1
|
||||
version: 2.8.2
|
||||
@@ -409,9 +409,12 @@ importers:
|
||||
'@react-email/render':
|
||||
specifier: ^1.1.2
|
||||
version: 1.4.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@socket.io/redis-adapter':
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0(socket.io-adapter@2.5.6)
|
||||
'@socket.io/postgres-adapter':
|
||||
specifier: ^0.5.0
|
||||
version: 0.5.0(socket.io-adapter@2.5.6)
|
||||
'@types/pg':
|
||||
specifier: ^8.16.0
|
||||
version: 8.16.0
|
||||
ajv:
|
||||
specifier: ^8.17.1
|
||||
version: 8.18.0
|
||||
@@ -565,6 +568,9 @@ importers:
|
||||
socket.io:
|
||||
specifier: ^4.8.1
|
||||
version: 4.8.3
|
||||
socket.io-adapter:
|
||||
specifier: ^2.5.6
|
||||
version: 2.5.6
|
||||
tailwindcss-preset-email:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.1(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.2))
|
||||
@@ -673,7 +679,7 @@ importers:
|
||||
version: 13.15.10
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
eslint:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.2(jiti@2.6.1)
|
||||
@@ -730,7 +736,7 @@ importers:
|
||||
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
web:
|
||||
dependencies:
|
||||
@@ -755,9 +761,6 @@ importers:
|
||||
'@photo-sphere-viewer/core':
|
||||
specifier: ^5.14.0
|
||||
version: 5.14.1
|
||||
'@photo-sphere-viewer/equirectangular-tiles-adapter':
|
||||
specifier: ^5.14.1
|
||||
version: 5.14.1(@photo-sphere-viewer/core@5.14.1)
|
||||
'@photo-sphere-viewer/equirectangular-video-adapter':
|
||||
specifier: ^5.14.0
|
||||
version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))
|
||||
@@ -787,7 +790,7 @@ importers:
|
||||
version: 2.6.0
|
||||
fabric:
|
||||
specifier: ^7.0.0
|
||||
version: 7.2.0(encoding@0.1.13)
|
||||
version: 7.2.0
|
||||
geo-coordinates-parser:
|
||||
specifier: ^1.7.4
|
||||
version: 1.7.4
|
||||
@@ -887,7 +890,7 @@ importers:
|
||||
version: 6.9.1
|
||||
'@testing-library/svelte':
|
||||
specifier: ^5.2.8
|
||||
version: 5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@testing-library/user-event':
|
||||
specifier: ^14.5.2
|
||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||
@@ -911,7 +914,7 @@ importers:
|
||||
version: 1.5.6
|
||||
'@vitest/coverage-v8':
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
dotenv:
|
||||
specifier: ^17.0.0
|
||||
version: 17.3.1
|
||||
@@ -974,7 +977,7 @@ importers:
|
||||
version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages:
|
||||
|
||||
@@ -3386,6 +3389,10 @@ packages:
|
||||
'@microsoft/tsdoc@0.16.0':
|
||||
resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==}
|
||||
|
||||
'@msgpack/msgpack@2.8.0':
|
||||
resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
|
||||
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
|
||||
cpu: [arm64]
|
||||
@@ -3889,11 +3896,6 @@ packages:
|
||||
'@photo-sphere-viewer/core@5.14.1':
|
||||
resolution: {integrity: sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==}
|
||||
|
||||
'@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.1':
|
||||
resolution: {integrity: sha512-QHd9y5cIFXAAZInbKbh+nUz5uzSRTiR8HYApm+ONlDu8JHAp410xNhB1vNm2Q1mVgg8IigQpD9Za5mPq10fESA==}
|
||||
peerDependencies:
|
||||
'@photo-sphere-viewer/core': 5.14.1
|
||||
|
||||
'@photo-sphere-viewer/equirectangular-video-adapter@5.14.1':
|
||||
resolution: {integrity: sha512-rZ6igEy1TEfgHB8Ak/8N0rZNYQLbNEGLVmhwNxDMWESCJ9nrNx3tJHFn7k6eZYjj9zJA73xF5YdY6XWUCpZDzg==}
|
||||
peerDependencies:
|
||||
@@ -4299,9 +4301,9 @@ packages:
|
||||
'@socket.io/component-emitter@3.1.2':
|
||||
resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==}
|
||||
|
||||
'@socket.io/redis-adapter@8.3.0':
|
||||
resolution: {integrity: sha512-ly0cra+48hDmChxmIpnESKrc94LjRL80TEmZVscuQ/WWkRP81nNj8W8cCGMqbI4L6NCuAaPRSzZF1a9GlAxxnA==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
'@socket.io/postgres-adapter@0.5.0':
|
||||
resolution: {integrity: sha512-s1vFsatB4lS429ZbeAi8ju+mZMgtgdSmi9UsZsdcEG++vVtX5z10yDEt4TV8saePscvvGjs6uXvJfMCxz8+M2Q==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
peerDependencies:
|
||||
socket.io-adapter: ^2.5.4
|
||||
|
||||
@@ -9255,9 +9257,6 @@ packages:
|
||||
not@0.1.0:
|
||||
resolution: {integrity: sha512-5PDmaAsVfnWUgTUbJ3ERwn7u79Z0dYxN9ErxCpVJJqe2RK0PJ3z+iFUxuqjwtlDDegXvtWoxD/3Fzxox7tFGWA==}
|
||||
|
||||
notepack.io@3.0.1:
|
||||
resolution: {integrity: sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==}
|
||||
|
||||
npm-run-path@4.0.1:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -11562,10 +11561,6 @@ packages:
|
||||
engines: {node: '>=0.8.0'}
|
||||
hasBin: true
|
||||
|
||||
uid2@1.0.0:
|
||||
resolution: {integrity: sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
|
||||
uid@2.0.2:
|
||||
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -15198,6 +15193,22 @@ snapshots:
|
||||
|
||||
'@mapbox/mapbox-gl-rtl-text@0.3.0': {}
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
https-proxy-agent: 5.0.1
|
||||
make-dir: 3.1.0
|
||||
node-fetch: 2.7.0
|
||||
nopt: 5.0.0
|
||||
npmlog: 5.0.1
|
||||
rimraf: 3.0.2
|
||||
semver: 7.7.4
|
||||
tar: 6.2.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
@@ -15307,6 +15318,8 @@ snapshots:
|
||||
|
||||
'@microsoft/tsdoc@0.16.0': {}
|
||||
|
||||
'@msgpack/msgpack@2.8.0': {}
|
||||
|
||||
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
|
||||
optional: true
|
||||
|
||||
@@ -15873,10 +15886,6 @@ snapshots:
|
||||
dependencies:
|
||||
three: 0.179.1
|
||||
|
||||
'@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)':
|
||||
dependencies:
|
||||
'@photo-sphere-viewer/core': 5.14.1
|
||||
|
||||
'@photo-sphere-viewer/equirectangular-video-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))':
|
||||
dependencies:
|
||||
'@photo-sphere-viewer/core': 5.14.1
|
||||
@@ -16189,13 +16198,15 @@ snapshots:
|
||||
|
||||
'@socket.io/component-emitter@3.1.2': {}
|
||||
|
||||
'@socket.io/redis-adapter@8.3.0(socket.io-adapter@2.5.6)':
|
||||
'@socket.io/postgres-adapter@0.5.0(socket.io-adapter@2.5.6)':
|
||||
dependencies:
|
||||
'@msgpack/msgpack': 2.8.0
|
||||
'@types/pg': 8.16.0
|
||||
debug: 4.3.7
|
||||
notepack.io: 3.0.1
|
||||
pg: 8.18.0
|
||||
socket.io-adapter: 2.5.6
|
||||
uid2: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- pg-native
|
||||
- supports-color
|
||||
|
||||
'@sphinxxxx/color-conversion@2.2.2': {}
|
||||
@@ -16512,14 +16523,14 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.53.5
|
||||
|
||||
'@testing-library/svelte@5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@testing-library/svelte@5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@testing-library/dom': 10.4.1
|
||||
'@testing-library/svelte-core': 1.0.0(svelte@5.53.5)
|
||||
svelte: 5.53.5
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||
dependencies:
|
||||
@@ -17213,7 +17224,7 @@ snapshots:
|
||||
|
||||
'@vercel/oidc@3.0.5': {}
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -17228,11 +17239,11 @@ snapshots:
|
||||
std-env: 3.10.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@bcoe/v8-coverage': 1.0.2
|
||||
@@ -17247,7 +17258,7 @@ snapshots:
|
||||
std-env: 3.10.0
|
||||
test-exclude: 7.0.1
|
||||
tinyrainbow: 2.0.0
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -17974,6 +17985,16 @@ snapshots:
|
||||
|
||||
caniuse-lite@1.0.30001774: {}
|
||||
|
||||
canvas@2.11.2:
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11
|
||||
nan: 2.25.0
|
||||
simple-get: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
optional: true
|
||||
|
||||
canvas@2.11.2(encoding@0.1.13):
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
|
||||
@@ -19571,10 +19592,10 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fabric@7.2.0(encoding@0.1.13):
|
||||
fabric@7.2.0:
|
||||
optionalDependencies:
|
||||
canvas: 2.11.2(encoding@0.1.13)
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
canvas: 2.11.2
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- encoding
|
||||
@@ -20753,6 +20774,36 @@ snapshots:
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsdom@26.1.0(canvas@2.11.2):
|
||||
dependencies:
|
||||
cssstyle: 4.6.0
|
||||
data-urls: 5.0.0
|
||||
decimal.js: 10.6.0
|
||||
html-encoding-sniffer: 4.0.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
is-potential-custom-element-name: 1.0.1
|
||||
nwsapi: 2.2.23
|
||||
parse5: 7.3.0
|
||||
rrweb-cssom: 0.8.0
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 5.1.2
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 7.0.0
|
||||
whatwg-encoding: 3.1.1
|
||||
whatwg-mimetype: 4.0.0
|
||||
whatwg-url: 14.2.0
|
||||
ws: 8.19.0
|
||||
xml-name-validator: 5.0.0
|
||||
optionalDependencies:
|
||||
canvas: 2.11.2
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- supports-color
|
||||
- utf-8-validate
|
||||
optional: true
|
||||
|
||||
jsep@1.4.0: {}
|
||||
|
||||
jsesc@3.1.0: {}
|
||||
@@ -21998,6 +22049,11 @@ snapshots:
|
||||
emojilib: 2.4.0
|
||||
skin-tone: 2.0.0
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
optional: true
|
||||
|
||||
node-fetch@2.7.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
@@ -22050,8 +22106,6 @@ snapshots:
|
||||
|
||||
not@0.1.0: {}
|
||||
|
||||
notepack.io@3.0.1: {}
|
||||
|
||||
npm-run-path@4.0.1:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
@@ -24779,8 +24833,6 @@ snapshots:
|
||||
uglify-js@3.19.3:
|
||||
optional: true
|
||||
|
||||
uid2@1.0.0: {}
|
||||
|
||||
uid@2.0.2:
|
||||
dependencies:
|
||||
'@lukeed/csprng': 1.1.0
|
||||
@@ -25122,9 +25174,9 @@ snapshots:
|
||||
optionalDependencies:
|
||||
vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
@@ -25170,7 +25222,51 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@vitest/spy': 3.2.4
|
||||
'@vitest/utils': 3.2.4
|
||||
chai: 5.3.3
|
||||
debug: 4.4.3
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
picomatch: 4.0.3
|
||||
std-env: 3.10.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
vite-node: 3.2.4(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 24.11.0
|
||||
happy-dom: 20.6.3
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
@@ -25199,7 +25295,7 @@ snapshots:
|
||||
'@types/debug': 4.1.12
|
||||
'@types/node': 25.3.0
|
||||
happy-dom: 20.6.3
|
||||
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||
jsdom: 26.1.0(canvas@2.11.2)
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@react-email/components": "^0.5.0",
|
||||
"@react-email/render": "^1.1.2",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"@socket.io/postgres-adapter": "^0.5.0",
|
||||
"@types/pg": "^8.16.0",
|
||||
"ajv": "^8.17.1",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
@@ -109,6 +110,7 @@
|
||||
"sharp": "^0.34.5",
|
||||
"sirv": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"socket.io-adapter": "^2.5.6",
|
||||
"tailwindcss-preset-email": "^1.4.0",
|
||||
"thumbhash": "^0.1.1",
|
||||
"transformation-matrix": "^3.1.0",
|
||||
|
||||
@@ -5,8 +5,9 @@ import cookieParser from 'cookie-parser';
|
||||
import { existsSync } from 'node:fs';
|
||||
import sirv from 'sirv';
|
||||
import { excludePaths, serverVersion } from 'src/constants';
|
||||
import { SocketIoAdapter } from 'src/enum';
|
||||
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
|
||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { createWebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
|
||||
@@ -25,6 +26,7 @@ export async function configureExpress(
|
||||
{
|
||||
permitSwaggerWrite = true,
|
||||
ssr,
|
||||
socketIoAdapter,
|
||||
}: {
|
||||
/**
|
||||
* Whether to allow swagger module to write to the specs.json
|
||||
@@ -36,6 +38,10 @@ export async function configureExpress(
|
||||
* Service to use for server-side rendering
|
||||
*/
|
||||
ssr: typeof ApiService | typeof MaintenanceWorkerService;
|
||||
/**
|
||||
* Override the Socket.IO adapter. If not specified, uses the adapter from config.
|
||||
*/
|
||||
socketIoAdapter?: SocketIoAdapter;
|
||||
},
|
||||
) {
|
||||
const configRepository = app.get(ConfigRepository);
|
||||
@@ -55,7 +61,7 @@ export async function configureExpress(
|
||||
}
|
||||
|
||||
app.setGlobalPrefix('api', { exclude: excludePaths });
|
||||
app.useWebSocketAdapter(new WebSocketAdapter(app));
|
||||
app.useWebSocketAdapter(await createWebSocketAdapter(app, socketIoAdapter));
|
||||
|
||||
useSwagger(app, { write: configRepository.isDev() && permitSwaggerWrite });
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||
|
||||
export const FACE_THUMBNAIL_SIZE = 250;
|
||||
export const TILE_TARGET_SIZE = 1024;
|
||||
|
||||
type ModelInfo = { dimSize: number };
|
||||
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Next,
|
||||
Param,
|
||||
ParseFilePipe,
|
||||
ParseIntPipe,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
@@ -186,29 +185,6 @@ export class AssetMediaController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/tiles/:level/:col/:row')
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||
@Endpoint({
|
||||
summary: 'View an asset tile',
|
||||
description: 'Download a specific tile from an image at the specified level - must currently be 0 - and position',
|
||||
history: new HistoryBuilder().added('v2.7.0').stable('v2.7.0'),
|
||||
})
|
||||
async viewAssetTile(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Param('level', ParseIntPipe) level: number,
|
||||
@Param('col', ParseIntPipe) col: number,
|
||||
@Param('row', ParseIntPipe) row: number,
|
||||
@Res() res: Response,
|
||||
@Next() next: NextFunction,
|
||||
) {
|
||||
if (level !== 0) {
|
||||
throw new Error(`Invalid level ${level}`);
|
||||
}
|
||||
await sendFile(res, next, () => this.service.viewAssetTile(auth, id, level, col, row), this.logger);
|
||||
}
|
||||
|
||||
@Get(':id/video/playback')
|
||||
@FileResponse()
|
||||
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DatabaseBackupController } from 'src/controllers/database-backup.contro
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||
import { FaceController } from 'src/controllers/face.controller';
|
||||
import { InternalController } from 'src/controllers/internal.controller';
|
||||
import { JobController } from 'src/controllers/job.controller';
|
||||
import { LibraryController } from 'src/controllers/library.controller';
|
||||
import { MaintenanceController } from 'src/controllers/maintenance.controller';
|
||||
@@ -51,6 +52,7 @@ export const controllers = [
|
||||
DownloadController,
|
||||
DuplicateController,
|
||||
FaceController,
|
||||
InternalController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
MaintenanceController,
|
||||
|
||||
22
server/src/controllers/internal.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Body, Controller, NotFoundException, Post, Req } from '@nestjs/common';
|
||||
import { ApiExcludeController } from '@nestjs/swagger';
|
||||
import { Request } from 'express';
|
||||
import { AppRestartEvent, EventRepository } from 'src/repositories/event.repository';
|
||||
|
||||
const LOCALHOST_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
||||
|
||||
@ApiExcludeController()
|
||||
@Controller('internal')
|
||||
export class InternalController {
|
||||
constructor(private eventRepository: EventRepository) {}
|
||||
|
||||
@Post('restart')
|
||||
async restart(@Req() req: Request, @Body() dto: AppRestartEvent): Promise<void> {
|
||||
const remoteAddress = req.socket.remoteAddress;
|
||||
if (!remoteAddress || !LOCALHOST_ADDRESSES.has(remoteAddress)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await this.eventRepository.emit('AppRestart', dto);
|
||||
}
|
||||
}
|
||||
@@ -120,10 +120,6 @@ export class StorageCore {
|
||||
);
|
||||
}
|
||||
|
||||
static getTilesFolder(asset: ThumbnailPathEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}_tiles`);
|
||||
}
|
||||
|
||||
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`);
|
||||
}
|
||||
@@ -157,16 +153,6 @@ export class StorageCore {
|
||||
});
|
||||
}
|
||||
|
||||
async moveAssetTiles(asset: StorageAsset) {
|
||||
const oldDir = getAssetFile(asset.files, AssetFileType.Tiles, { isEdited: false });
|
||||
return this.moveFile({
|
||||
entityId: asset.id,
|
||||
pathType: AssetPathType.Tiles,
|
||||
oldPath: oldDir?.path || null,
|
||||
newPath: StorageCore.getTilesFolder(asset),
|
||||
})
|
||||
}
|
||||
|
||||
async moveAssetVideo(asset: StorageAsset) {
|
||||
return this.moveFile({
|
||||
entityId: asset.id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsString, Matches } from 'class-validator';
|
||||
import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum';
|
||||
import { ImmichEnvironment, LogFormat, LogLevel, SocketIoAdapter } from 'src/enum';
|
||||
import { IsIPRange, Optional, ValidateBoolean } from 'src/validation';
|
||||
|
||||
// TODO import from sql-tools once the swagger plugin supports external enums
|
||||
@@ -149,6 +149,11 @@ export class EnvDto {
|
||||
@Optional()
|
||||
IMMICH_WORKERS_EXCLUDE?: string;
|
||||
|
||||
@IsEnum(SocketIoAdapter)
|
||||
@Optional()
|
||||
@Transform(({ value }) => (value ? String(value).toLowerCase().trim() : value))
|
||||
IMMICH_SOCKETIO_ADAPTER?: SocketIoAdapter;
|
||||
|
||||
@IsString()
|
||||
@Optional()
|
||||
DB_DATABASE_NAME?: string;
|
||||
|
||||
@@ -45,8 +45,6 @@ export enum AssetFileType {
|
||||
Preview = 'preview',
|
||||
Thumbnail = 'thumbnail',
|
||||
Sidecar = 'sidecar',
|
||||
/** Folder structure containing tiles of the image */
|
||||
Tiles = 'tiles',
|
||||
}
|
||||
|
||||
export enum AlbumUserRole {
|
||||
@@ -373,8 +371,6 @@ export enum ManualJobName {
|
||||
|
||||
export enum AssetPathType {
|
||||
Original = 'original',
|
||||
/** Folder structure containing tiles of the image */
|
||||
Tiles = 'tiles',
|
||||
EncodedVideo = 'encoded_video',
|
||||
}
|
||||
|
||||
@@ -522,6 +518,11 @@ export enum ImmichTelemetry {
|
||||
Job = 'job',
|
||||
}
|
||||
|
||||
export enum SocketIoAdapter {
|
||||
BroadcastChannel = 'broadcastchannel',
|
||||
Postgres = 'postgres',
|
||||
}
|
||||
|
||||
export enum ExifOrientation {
|
||||
Horizontal = 1,
|
||||
MirrorHorizontal = 2,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
import { CommandFactory } from 'nest-commander';
|
||||
import { ChildProcess, fork } from 'node:child_process';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import { PostgresError } from 'postgres';
|
||||
@@ -18,7 +17,7 @@ class Workers {
|
||||
/**
|
||||
* Currently running workers
|
||||
*/
|
||||
workers: Partial<Record<ImmichWorker, { kill: (signal: NodeJS.Signals) => Promise<void> | void }>> = {};
|
||||
workers: Partial<Record<ImmichWorker, { kill: () => Promise<void> | void }>> = {};
|
||||
|
||||
/**
|
||||
* Fail-safe in case anything dies during restart
|
||||
@@ -101,25 +100,23 @@ class Workers {
|
||||
const basePath = dirname(__filename);
|
||||
const workerFile = join(basePath, 'workers', `${name}.js`);
|
||||
|
||||
let anyWorker: Worker | ChildProcess;
|
||||
let kill: (signal?: NodeJS.Signals) => Promise<void> | void;
|
||||
const inspectArg = process.execArgv.find((arg) => arg.startsWith('--inspect'));
|
||||
const workerData: { inspectorPort?: number } = {};
|
||||
|
||||
if (name === ImmichWorker.Api) {
|
||||
const worker = fork(workerFile, [], {
|
||||
execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)),
|
||||
});
|
||||
|
||||
kill = (signal) => void worker.kill(signal);
|
||||
anyWorker = worker;
|
||||
} else {
|
||||
const worker = new Worker(workerFile);
|
||||
|
||||
kill = async () => void (await worker.terminate());
|
||||
anyWorker = worker;
|
||||
if (inspectArg) {
|
||||
const inspectorPorts: Record<ImmichWorker, number> = {
|
||||
[ImmichWorker.Api]: 9230,
|
||||
[ImmichWorker.Microservices]: 9231,
|
||||
[ImmichWorker.Maintenance]: 9232,
|
||||
};
|
||||
workerData.inspectorPort = inspectorPorts[name];
|
||||
}
|
||||
|
||||
anyWorker.on('error', (error) => this.onError(name, error));
|
||||
anyWorker.on('exit', (exitCode) => this.onExit(name, exitCode));
|
||||
const worker = new Worker(workerFile, { workerData });
|
||||
const kill = async () => void (await worker.terminate());
|
||||
|
||||
worker.on('error', (error) => this.onError(name, error));
|
||||
worker.on('exit', (exitCode) => this.onExit(name, exitCode));
|
||||
|
||||
this.workers[name] = { kill };
|
||||
}
|
||||
@@ -152,8 +149,8 @@ class Workers {
|
||||
console.error(`${name} worker exited with code ${exitCode}`);
|
||||
|
||||
if (this.workers[ImmichWorker.Api] && name !== ImmichWorker.Api) {
|
||||
console.error('Killing api process');
|
||||
void this.workers[ImmichWorker.Api].kill('SIGTERM');
|
||||
console.error('Terminating api worker');
|
||||
void this.workers[ImmichWorker.Api].kill();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Delete,
|
||||
Get,
|
||||
Next,
|
||||
NotFoundException,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
@@ -25,12 +26,15 @@ import { ImmichCookie } from 'src/enum';
|
||||
import { MaintenanceRoute } from 'src/maintenance/maintenance-auth.guard';
|
||||
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
|
||||
import { GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { FilenameParamDto } from 'src/validation';
|
||||
|
||||
const LOCALHOST_ADDRESSES = new Set(['127.0.0.1', '::1', '::ffff:127.0.0.1']);
|
||||
|
||||
import type { DatabaseBackupController as _DatabaseBackupController } from 'src/controllers/database-backup.controller';
|
||||
import type { ServerController as _ServerController } from 'src/controllers/server.controller';
|
||||
import { DatabaseBackupDeleteDto, DatabaseBackupListResponseDto } from 'src/dtos/database-backup.dto';
|
||||
@@ -131,4 +135,14 @@ export class MaintenanceWorkerController {
|
||||
setMaintenanceMode(@Body() dto: SetMaintenanceModeDto): void {
|
||||
void this.service.setAction(dto);
|
||||
}
|
||||
|
||||
@Post('internal/restart')
|
||||
internalRestart(@Req() req: Request, @Body() dto: AppRestartEvent): void {
|
||||
const remoteAddress = req.socket.remoteAddress;
|
||||
if (!remoteAddress || !LOCALHOST_ADDRESSES.has(remoteAddress)) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
this.service.handleInternalRestart(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { MaintenanceWebsocketRepository } from 'src/maintenance/maintenance-webs
|
||||
import { AppRepository } from 'src/repositories/app.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { ProcessRepository } from 'src/repositories/process.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
@@ -290,6 +291,9 @@ export class MaintenanceWorkerService {
|
||||
|
||||
const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
|
||||
if (!lock) {
|
||||
// Another maintenance worker has the lock - poll until maintenance mode ends
|
||||
this.logger.log('Another worker has the maintenance lock, polling for maintenance mode changes...');
|
||||
await this.pollForMaintenanceEnd();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -351,4 +355,25 @@ export class MaintenanceWorkerService {
|
||||
this.maintenanceWebsocketRepository.serverSend('AppRestart', state);
|
||||
this.appRepository.exitApp();
|
||||
}
|
||||
|
||||
handleInternalRestart(state: AppRestartEvent): void {
|
||||
this.maintenanceWebsocketRepository.clientBroadcast('AppRestartV1', state);
|
||||
this.maintenanceWebsocketRepository.serverSend('AppRestart', state);
|
||||
this.appRepository.exitApp();
|
||||
}
|
||||
|
||||
private async pollForMaintenanceEnd(): Promise<void> {
|
||||
const pollIntervalMs = 5000;
|
||||
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
|
||||
const state = await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode);
|
||||
if (!state?.isMaintenanceMode) {
|
||||
this.logger.log('Maintenance mode ended, restarting...');
|
||||
this.appRepository.exitApp();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
server/src/middleware/broadcast-channel.adapter.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
ClusterAdapterWithHeartbeat,
|
||||
type ClusterAdapterOptions,
|
||||
type ClusterMessage,
|
||||
type ClusterResponse,
|
||||
type ServerId,
|
||||
} from 'socket.io-adapter';
|
||||
|
||||
const BC_CHANNEL_NAME = 'immich:socketio';
|
||||
|
||||
interface BroadcastChannelPayload {
|
||||
type: 'message' | 'response';
|
||||
sourceUid: string;
|
||||
targetUid?: string;
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Socket.IO adapter using Node.js BroadcastChannel
|
||||
*
|
||||
* Relays messages between worker_threads within a single OS process.
|
||||
* Zero external dependencies. Does NOT work across containers — use
|
||||
* the Postgres adapter for multi-replica deployments.
|
||||
*/
|
||||
class BroadcastChannelAdapter extends ClusterAdapterWithHeartbeat {
|
||||
private readonly channel: BroadcastChannel;
|
||||
|
||||
constructor(nsp: any, opts?: Partial<ClusterAdapterOptions>) {
|
||||
super(nsp, opts ?? {});
|
||||
|
||||
this.channel = new BroadcastChannel(BC_CHANNEL_NAME);
|
||||
this.channel.addEventListener('message', (event: MessageEvent<BroadcastChannelPayload>) => {
|
||||
const msg = event.data;
|
||||
if (msg.sourceUid === this.uid) {
|
||||
return;
|
||||
}
|
||||
if (msg.type === 'message') {
|
||||
this.onMessage(msg.data as ClusterMessage);
|
||||
} else if (msg.type === 'response' && msg.targetUid === this.uid) {
|
||||
this.onResponse(msg.data as ClusterResponse);
|
||||
}
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
override doPublish(message: ClusterMessage): Promise<string> {
|
||||
this.channel.postMessage({
|
||||
type: 'message',
|
||||
sourceUid: this.uid,
|
||||
data: message,
|
||||
});
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
override doPublishResponse(requesterUid: ServerId, response: ClusterResponse): Promise<void> {
|
||||
this.channel.postMessage({
|
||||
type: 'response',
|
||||
sourceUid: this.uid,
|
||||
targetUid: requesterUid,
|
||||
data: response,
|
||||
});
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
override close(): void {
|
||||
super.close();
|
||||
this.channel.close();
|
||||
}
|
||||
}
|
||||
|
||||
export function createBroadcastChannelAdapter(opts?: Partial<ClusterAdapterOptions>) {
|
||||
const options: Partial<ClusterAdapterOptions> = {
|
||||
...opts,
|
||||
};
|
||||
|
||||
return function (nsp: any) {
|
||||
return new BroadcastChannelAdapter(nsp, options);
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,103 @@
|
||||
import { INestApplicationContext } from '@nestjs/common';
|
||||
import { INestApplication, Logger } from '@nestjs/common';
|
||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import { Redis } from 'ioredis';
|
||||
import { ServerOptions } from 'socket.io';
|
||||
import { Pool, PoolConfig } from 'pg';
|
||||
import type { ServerOptions } from 'socket.io';
|
||||
import { SocketIoAdapter } from 'src/enum';
|
||||
import { createBroadcastChannelAdapter } from 'src/middleware/broadcast-channel.adapter';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { asPostgresConnectionConfig } from 'src/utils/database';
|
||||
|
||||
export class WebSocketAdapter extends IoAdapter {
|
||||
constructor(private app: INestApplicationContext) {
|
||||
export type Ssl = 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
|
||||
|
||||
export function asPgPoolSsl(ssl?: Ssl): PoolConfig['ssl'] {
|
||||
if (ssl === undefined || ssl === false || ssl === 'allow') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ssl === true || ssl === 'prefer' || ssl === 'require') {
|
||||
return { rejectUnauthorized: false };
|
||||
}
|
||||
|
||||
if (ssl === 'verify-full') {
|
||||
return { rejectUnauthorized: true };
|
||||
}
|
||||
|
||||
return ssl;
|
||||
}
|
||||
|
||||
class BroadcastChannelSocketAdapter extends IoAdapter {
|
||||
private adapterConstructor: ReturnType<typeof createBroadcastChannelAdapter>;
|
||||
|
||||
constructor(app: INestApplication) {
|
||||
super(app);
|
||||
this.adapterConstructor = createBroadcastChannelAdapter();
|
||||
}
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const { redis } = this.app.get(ConfigRepository).getEnv();
|
||||
const server = super.createIOServer(port, options);
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
server.adapter(this.adapterConstructor);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
class PostgresSocketAdapter extends IoAdapter {
|
||||
private adapterConstructor: any;
|
||||
|
||||
constructor(app: INestApplication, adapterConstructor: any) {
|
||||
super(app);
|
||||
this.adapterConstructor = adapterConstructor;
|
||||
}
|
||||
|
||||
createIOServer(port: number, options?: ServerOptions): any {
|
||||
const server = super.createIOServer(port, options);
|
||||
server.adapter(this.adapterConstructor);
|
||||
return server;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWebSocketAdapter(
|
||||
app: INestApplication,
|
||||
adapterOverride?: SocketIoAdapter,
|
||||
): Promise<IoAdapter> {
|
||||
const logger = new Logger('WebSocketAdapter');
|
||||
const config = new ConfigRepository();
|
||||
const { database, socketIo } = config.getEnv();
|
||||
const adapter = adapterOverride ?? socketIo.adapter;
|
||||
|
||||
switch (adapter) {
|
||||
case SocketIoAdapter.Postgres: {
|
||||
logger.log('Using Postgres Socket.IO adapter');
|
||||
const { createAdapter } = await import('@socket.io/postgres-adapter');
|
||||
const config = asPostgresConnectionConfig(database.config);
|
||||
const pool = new Pool({
|
||||
host: config.host,
|
||||
port: config.port,
|
||||
user: config.username,
|
||||
password: config.password,
|
||||
database: config.database,
|
||||
ssl: asPgPoolSsl(config.ssl),
|
||||
max: 2,
|
||||
});
|
||||
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS socket_io_attachments (
|
||||
id bigserial UNIQUE,
|
||||
created_at timestamptz DEFAULT NOW(),
|
||||
payload bytea
|
||||
);
|
||||
`);
|
||||
|
||||
pool.on('error', (error) => {
|
||||
logger.error(' Postgres pool error', error);
|
||||
});
|
||||
|
||||
const adapterConstructor = createAdapter(pool);
|
||||
return new PostgresSocketAdapter(app, adapterConstructor);
|
||||
}
|
||||
|
||||
case SocketIoAdapter.BroadcastChannel: {
|
||||
logger.log('Using BroadcastChannel Socket.IO adapter');
|
||||
return new BroadcastChannelSocketAdapter(app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,30 +102,22 @@ order by
|
||||
"shared_link"."createdAt" desc
|
||||
|
||||
-- SharedLinkRepository.getAll
|
||||
select
|
||||
"shared_link".*,
|
||||
(
|
||||
select
|
||||
coalesce(json_agg(agg), '[]')
|
||||
from
|
||||
(
|
||||
select
|
||||
"asset".*
|
||||
from
|
||||
"shared_link_asset"
|
||||
inner join "asset" on "asset"."id" = "shared_link_asset"."assetId"
|
||||
where
|
||||
"shared_link"."id" = "shared_link_asset"."sharedLinkId"
|
||||
and "asset"."deletedAt" is null
|
||||
order by
|
||||
"asset"."fileCreatedAt" asc
|
||||
limit
|
||||
$1
|
||||
) as agg
|
||||
) as "assets",
|
||||
select distinct
|
||||
on ("shared_link"."createdAt") "shared_link".*,
|
||||
"assets"."assets",
|
||||
to_json("album") as "album"
|
||||
from
|
||||
"shared_link"
|
||||
left join "shared_link_asset" on "shared_link_asset"."sharedLinkId" = "shared_link"."id"
|
||||
left join lateral (
|
||||
select
|
||||
json_agg("asset") as "assets"
|
||||
from
|
||||
"asset"
|
||||
where
|
||||
"asset"."id" = "shared_link_asset"."assetId"
|
||||
and "asset"."deletedAt" is null
|
||||
) as "assets" on true
|
||||
left join lateral (
|
||||
select
|
||||
"album".*,
|
||||
@@ -160,12 +152,12 @@ from
|
||||
and "album"."deletedAt" is null
|
||||
) as "album" on true
|
||||
where
|
||||
"shared_link"."userId" = $2
|
||||
"shared_link"."userId" = $1
|
||||
and (
|
||||
"shared_link"."type" = $3
|
||||
"shared_link"."type" = $2
|
||||
or "album"."id" is not null
|
||||
)
|
||||
and "shared_link"."albumId" = $4
|
||||
and "shared_link"."albumId" = $3
|
||||
order by
|
||||
"shared_link"."createdAt" desc
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import Redis from 'ioredis';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
import { ExitCode } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
@@ -24,24 +21,17 @@ export class AppRepository {
|
||||
}
|
||||
|
||||
async sendOneShotAppRestart(state: AppRestartEvent): Promise<void> {
|
||||
const server = new SocketIO();
|
||||
const { redis } = new ConfigRepository().getEnv();
|
||||
const pubClient = new Redis({ ...redis, lazyConnect: true });
|
||||
const subClient = pubClient.duplicate();
|
||||
const { port } = new ConfigRepository().getEnv();
|
||||
const url = `http://127.0.0.1:${port}/api/internal/restart`;
|
||||
|
||||
await Promise.all([pubClient.connect(), subClient.connect()]);
|
||||
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
server.emit('AppRestartV1', state, async () => {
|
||||
const responses = await server.serverSideEmitWithAck('AppRestart', state);
|
||||
if (responses.some((response) => response !== 'ok')) {
|
||||
throw new Error("One or more node(s) returned a non-'ok' response to our restart request!");
|
||||
}
|
||||
|
||||
pubClient.disconnect();
|
||||
subClient.disconnect();
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(state),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to trigger app restart: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1128,19 +1128,6 @@ export class AssetRepository {
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getForTiles(id: string) {
|
||||
// TODO: we don't actually need original path and file name. Plain 'select asset_file.path from asset_file where type = tiles and assetId = id;'?
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.where('asset.id', '=', id)
|
||||
.leftJoin('asset_file', (join) =>
|
||||
join.onRef('asset.id', '=', 'asset_file.assetId').on('asset_file.type', '=', AssetFileType.Tiles),
|
||||
)
|
||||
.select(['asset.originalPath', 'asset.originalFileName', 'asset_file.path as path'])
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
async getForVideo(id: string) {
|
||||
return this.db
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
LogFormat,
|
||||
LogLevel,
|
||||
QueueName,
|
||||
SocketIoAdapter,
|
||||
} from 'src/enum';
|
||||
import { VectorExtension } from 'src/types';
|
||||
import { setDifference } from 'src/utils/set';
|
||||
@@ -117,6 +118,10 @@ export interface EnvData {
|
||||
};
|
||||
};
|
||||
|
||||
socketIo: {
|
||||
adapter: SocketIoAdapter;
|
||||
};
|
||||
|
||||
noColor: boolean;
|
||||
nodeVersion?: string;
|
||||
}
|
||||
@@ -347,6 +352,10 @@ const getEnv = (): EnvData => {
|
||||
},
|
||||
},
|
||||
|
||||
socketIo: {
|
||||
adapter: dto.IMMICH_SOCKETIO_ADAPTER ?? SocketIoAdapter.Postgres,
|
||||
},
|
||||
|
||||
noColor: !!dto.NO_COLOR,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -182,24 +182,6 @@ export class MediaRepository {
|
||||
await decoded.toFile(output);
|
||||
}
|
||||
|
||||
/**
|
||||
* For output file path 'output.dz', this creates an 'output.dzi' file and 'output_files/0' directory containing tiles
|
||||
*/
|
||||
async generateTiles(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise<void> {
|
||||
// size is intended tile size, don't resize input image.
|
||||
const pipeline = await this.getImageDecodingPipeline(input, { ...options, size: undefined });
|
||||
await pipeline
|
||||
.toFormat(options.format) // TODO: set quality and chroma ss?
|
||||
.tile({
|
||||
depth: 'one',
|
||||
size: options.size,
|
||||
})
|
||||
.toFile(output);
|
||||
// TODO: move <uuid>_tiles_files/0 dir to <uuid>_tiles
|
||||
// TODO: delete <uuid>_tiles_files/vips-properties.xml
|
||||
// TODO: delete <uuid>_tiles.dzi
|
||||
}
|
||||
|
||||
private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||
let pipeline = sharp(input, {
|
||||
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Insertable, Kysely, sql, Updateable } from 'kysely';
|
||||
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import { Insertable, Kysely, NotNull, sql, Updateable } from 'kysely';
|
||||
import { jsonObjectFrom } from 'kysely/helpers/postgres';
|
||||
import _ from 'lodash';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { Album, columns } from 'src/database';
|
||||
@@ -124,20 +124,19 @@ export class SharedLinkRepository {
|
||||
.selectFrom('shared_link')
|
||||
.selectAll('shared_link')
|
||||
.where('shared_link.userId', '=', userId)
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
.leftJoin('shared_link_asset', 'shared_link_asset.sharedLinkId', 'shared_link.id')
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
.selectFrom('shared_link_asset')
|
||||
.whereRef('shared_link.id', '=', 'shared_link_asset.sharedLinkId')
|
||||
.innerJoin('asset', 'asset.id', 'shared_link_asset.assetId')
|
||||
.selectFrom('asset')
|
||||
.select((eb) => eb.fn.jsonAgg('asset').as('assets'))
|
||||
.whereRef('asset.id', '=', 'shared_link_asset.assetId')
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.selectAll('asset')
|
||||
.orderBy('asset.fileCreatedAt', 'asc')
|
||||
.limit(1),
|
||||
)
|
||||
.$castTo<MapAsset[]>()
|
||||
.as('assets'),
|
||||
.as('assets'),
|
||||
(join) => join.onTrue(),
|
||||
)
|
||||
.select('assets.assets')
|
||||
.$narrowType<{ assets: NotNull }>()
|
||||
.leftJoinLateral(
|
||||
(eb) =>
|
||||
eb
|
||||
@@ -180,6 +179,7 @@ export class SharedLinkRepository {
|
||||
.$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!))
|
||||
.$if(!!id, (eb) => eb.where('shared_link.id', '=', id!))
|
||||
.orderBy('shared_link.createdAt', 'desc')
|
||||
.distinctOn(['shared_link.createdAt'])
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
||||
@@ -720,38 +720,6 @@ describe(AssetMediaService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAssetTile', () => {
|
||||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.viewAssetTile(authStub.admin, 'id', 0, 0, 0)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']), undefined);
|
||||
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||
});
|
||||
|
||||
it('should throw an error if the asset tiles dir could not be found', async () => {
|
||||
const asset = AssetFactory.create();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForTiles.mockResolvedValue({ ...asset, path: null });
|
||||
|
||||
await expect(sut.viewAssetTile(authStub.admin, asset.id, 0, 0, 0)).rejects.toBeInstanceOf(NotFoundException);
|
||||
});
|
||||
|
||||
it('should get tile file', async () => {
|
||||
const asset = AssetFactory.from().file({ type: AssetFileType.Tiles, path: '/path/to/asset_tiles' }).build();
|
||||
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||
mocks.asset.getForTiles.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||
await expect(sut.viewAssetTile(authStub.admin, asset.id, 0, 0, 0)).resolves.toEqual(
|
||||
new ImmichFileResponse({
|
||||
path: `${asset.files[0].path}_files/0/0_0.jpeg`,
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
contentType: 'image/jpeg',
|
||||
}),
|
||||
);
|
||||
expect(mocks.asset.getForTiles).toHaveBeenCalledWith(asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('playbackVideo', () => {
|
||||
it('should require asset.view permissions', async () => {
|
||||
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
@@ -262,26 +262,6 @@ export class AssetMediaService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
async viewAssetTile(auth: AuthDto, id: string, level: number, col: number, row: number): Promise<ImmichFileResponse> {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
||||
|
||||
// TODO: get tile info { width, height } and check against col, row to return NotFound instead of 500 when tile can't be found in sendFile.
|
||||
const { path } = await this.assetRepository.getForTiles(id);
|
||||
|
||||
if (!path) {
|
||||
throw new NotFoundException('Asset tiles not found');
|
||||
}
|
||||
|
||||
// By definition of the tiles format, it's always .jpeg; should ImageFormat.Jpeg be used?
|
||||
const tilePath = `${path}_files/${level}/${col}_${row}.jpeg`;
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path: tilePath,
|
||||
contentType: mimeTypes.lookup(tilePath),
|
||||
cacheControl: CacheControl.PrivateWithCache,
|
||||
});
|
||||
}
|
||||
|
||||
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
||||
|
||||
|
||||
@@ -1246,7 +1246,8 @@ describe(MediaService.name, () => {
|
||||
expect.any(String),
|
||||
);
|
||||
|
||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledExactlyOnceWith('XMP-GPano', asset.originalPath, expect.any(String));
|
||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith('XMP-GPano', asset.originalPath, expect.any(String));
|
||||
});
|
||||
|
||||
it('should respect encoding options when generating full-size preview', async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE, TILE_TARGET_SIZE } from 'src/constants';
|
||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
||||
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core';
|
||||
import { AssetFile, Exif } from 'src/database';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
DecodeToBufferOptions,
|
||||
GenerateThumbnailOptions,
|
||||
ImageDimensions,
|
||||
ImageOptions,
|
||||
JobItem,
|
||||
JobOf,
|
||||
VideoFormat,
|
||||
@@ -164,7 +163,6 @@ export class MediaService extends BaseService {
|
||||
await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format);
|
||||
await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format);
|
||||
await this.storageCore.moveAssetTiles(asset);
|
||||
await this.storageCore.moveAssetVideo(asset);
|
||||
|
||||
return JobStatus.Success;
|
||||
@@ -278,8 +276,8 @@ export class MediaService extends BaseService {
|
||||
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||
const generateFullsize =
|
||||
(image.fullsize.enabled && !mimeTypes.isWebSupportedImage(asset.originalPath)) ||
|
||||
asset.exifInfo.projectionType === 'EQUIRECTANGULAR' ||
|
||||
((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') &&
|
||||
!mimeTypes.isWebSupportedImage(asset.originalPath)) ||
|
||||
useEdits;
|
||||
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||
|
||||
@@ -385,60 +383,23 @@ export class MediaService extends BaseService {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: probably extract to helper method
|
||||
// TODO: handle cropped panoramas. Tile as normal but save some offset?
|
||||
let tileInfo: UpsertFileOptions | undefined;
|
||||
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') {
|
||||
// TODO: get uncropped width from asset (FullPanoWidthPixels if present). -> TODO find out why i wrote this down as a todo
|
||||
const originalSize = asset.exifInfo.exifImageWidth!;
|
||||
|
||||
// Get the number of tiles at the exact target size, rounded up (to at least 1 tile).
|
||||
const numTilesExact = Math.ceil(originalSize / TILE_TARGET_SIZE);
|
||||
// Then round up to the nearest power of 2 (photo-sphere-viewer requirement).
|
||||
const numTiles = Math.pow(2, Math.ceil(Math.log2(numTilesExact)));
|
||||
const tileSize = Math.ceil(originalSize / numTiles);
|
||||
|
||||
tileInfo = {
|
||||
assetId: asset.id,
|
||||
type: AssetFileType.Tiles,
|
||||
path: StorageCore.getTilesFolder(asset),
|
||||
isEdited: false,
|
||||
isProgressive: false,
|
||||
isTransparent: false,
|
||||
};
|
||||
const tilesOptions = {
|
||||
...baseOptions,
|
||||
quality: image.preview.quality,
|
||||
format: ImageFormat.Jpeg,
|
||||
size: tileSize,
|
||||
};
|
||||
promises.push(this.mediaRepository.generateTiles(data, tilesOptions, tileInfo.path));
|
||||
|
||||
console.log('Tile info for DB:', {
|
||||
width: originalSize,
|
||||
cols: numTiles,
|
||||
rows: numTiles / 2,
|
||||
});
|
||||
}
|
||||
|
||||
const outputs = await Promise.all(promises);
|
||||
|
||||
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') {
|
||||
await this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewFile.path);
|
||||
const promises = [
|
||||
this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewFile.path),
|
||||
fullsizeFile
|
||||
? this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, fullsizeFile.path)
|
||||
: Promise.resolve(),
|
||||
];
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
const decodedDimensions = { width: info.width, height: info.height };
|
||||
const fullsizeDimensions = useEdits ? getOutputDimensions(asset.edits, decodedDimensions) : decodedDimensions;
|
||||
const files = [previewFile, thumbnailFile];
|
||||
if (fullsizeFile) {
|
||||
files.push(fullsizeFile);
|
||||
}
|
||||
if (tileInfo) {
|
||||
files.push(tileInfo);
|
||||
}
|
||||
|
||||
return {
|
||||
files,
|
||||
files: fullsizeFile ? [previewFile, thumbnailFile, fullsizeFile] : [previewFile, thumbnailFile],
|
||||
thumbhash: outputs[0] as Buffer,
|
||||
fullsizeDimensions,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,6 @@ export const getAssetFiles = (files: AssetFile[]) => ({
|
||||
fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }),
|
||||
previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }),
|
||||
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }),
|
||||
tilesPath: getAssetFile(files, AssetFileType.Tiles, { isEdited: false }),
|
||||
sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }),
|
||||
|
||||
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }),
|
||||
|
||||
@@ -1,60 +1,11 @@
|
||||
import { createAdapter } from '@socket.io/redis-adapter';
|
||||
import Redis from 'ioredis';
|
||||
import { SignJWT } from 'jose';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { join } from 'node:path';
|
||||
import { Server as SocketIO } from 'socket.io';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { MaintenanceAuthDto, MaintenanceDetectInstallResponseDto } from 'src/dtos/maintenance.dto';
|
||||
import { StorageFolder } from 'src/enum';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||
|
||||
export function sendOneShotAppRestart(state: AppRestartEvent): void {
|
||||
const server = new SocketIO();
|
||||
const { redis } = new ConfigRepository().getEnv();
|
||||
const pubClient = new Redis(redis);
|
||||
const subClient = pubClient.duplicate();
|
||||
server.adapter(createAdapter(pubClient, subClient));
|
||||
|
||||
/**
|
||||
* Keep trying until we manage to stop Immich
|
||||
*
|
||||
* Sometimes there appear to be communication
|
||||
* issues between to the other servers.
|
||||
*
|
||||
* This issue only occurs with this method.
|
||||
*/
|
||||
async function tryTerminate() {
|
||||
while (true) {
|
||||
try {
|
||||
const responses = await server.serverSideEmitWithAck('AppRestart', state);
|
||||
if (responses.length > 0) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Encountered an error while telling Immich to stop.');
|
||||
}
|
||||
|
||||
console.info(
|
||||
"\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.",
|
||||
);
|
||||
|
||||
await new Promise((r) => setTimeout(r, 1e3));
|
||||
}
|
||||
}
|
||||
|
||||
// => corresponds to notification.service.ts#onAppRestart
|
||||
server.emit('AppRestartV1', state, () => {
|
||||
void tryTerminate().finally(() => {
|
||||
pubClient.disconnect();
|
||||
subClient.disconnect();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function createMaintenanceLoginUrl(
|
||||
baseUrl: string,
|
||||
auth: MaintenanceAuthDto,
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import inspector from 'node:inspector';
|
||||
import { isMainThread, workerData } from 'node:worker_threads';
|
||||
import { configureExpress, configureTelemetry } from 'src/app.common';
|
||||
import { ApiModule } from 'src/app.module';
|
||||
import { AppRepository } from 'src/repositories/app.repository';
|
||||
import { ApiService } from 'src/services/api.service';
|
||||
import { isStartUpError } from 'src/utils/misc';
|
||||
|
||||
async function bootstrap() {
|
||||
export async function bootstrap() {
|
||||
process.title = 'immich-api';
|
||||
|
||||
const { inspectorPort } = workerData ?? {};
|
||||
if (inspectorPort) {
|
||||
inspector.open(inspectorPort, '0.0.0.0', false);
|
||||
}
|
||||
|
||||
configureTelemetry();
|
||||
|
||||
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
|
||||
@@ -19,10 +26,12 @@ async function bootstrap() {
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap().catch((error) => {
|
||||
if (!isStartUpError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
});
|
||||
if (!isMainThread || process.send) {
|
||||
bootstrap().catch((error) => {
|
||||
if (!isStartUpError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import inspector from 'node:inspector';
|
||||
import { isMainThread, workerData } from 'node:worker_threads';
|
||||
import { configureExpress, configureTelemetry } from 'src/app.common';
|
||||
import { MaintenanceModule } from 'src/app.module';
|
||||
import { SocketIoAdapter } from 'src/enum';
|
||||
import { MaintenanceWorkerService } from 'src/maintenance/maintenance-worker.service';
|
||||
import { AppRepository } from 'src/repositories/app.repository';
|
||||
import { isStartUpError } from 'src/utils/misc';
|
||||
|
||||
async function bootstrap() {
|
||||
export async function bootstrap() {
|
||||
process.title = 'immich-maintenance';
|
||||
|
||||
const { inspectorPort } = workerData ?? {};
|
||||
if (inspectorPort) {
|
||||
inspector.open(inspectorPort, '0.0.0.0', false);
|
||||
}
|
||||
|
||||
configureTelemetry();
|
||||
|
||||
const app = await NestFactory.create<NestExpressApplication>(MaintenanceModule, { bufferLogs: true });
|
||||
@@ -16,13 +25,18 @@ async function bootstrap() {
|
||||
void configureExpress(app, {
|
||||
permitSwaggerWrite: false,
|
||||
ssr: MaintenanceWorkerService,
|
||||
// Use BroadcastChannel instead of Postgres adapter to avoid crash when
|
||||
// pg_terminate_backend() kills all database connections during restore
|
||||
socketIoAdapter: SocketIoAdapter.BroadcastChannel,
|
||||
});
|
||||
}
|
||||
|
||||
bootstrap().catch((error) => {
|
||||
if (!isStartUpError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
// eslint-disable-next-line unicorn/no-process-exit
|
||||
process.exit(1);
|
||||
});
|
||||
if (!isMainThread) {
|
||||
bootstrap().catch((error) => {
|
||||
if (!isStartUpError(error)) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { isMainThread } from 'node:worker_threads';
|
||||
import inspector from 'node:inspector';
|
||||
import { isMainThread, workerData } from 'node:worker_threads';
|
||||
import { MicroservicesModule } from 'src/app.module';
|
||||
import { serverVersion } from 'src/constants';
|
||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { createWebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { AppRepository } from 'src/repositories/app.repository';
|
||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
@@ -10,6 +11,11 @@ import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
|
||||
import { isStartUpError } from 'src/utils/misc';
|
||||
|
||||
export async function bootstrap() {
|
||||
const { inspectorPort } = workerData ?? {};
|
||||
if (inspectorPort) {
|
||||
inspector.open(inspectorPort, '0.0.0.0', false);
|
||||
}
|
||||
|
||||
const { telemetry } = new ConfigRepository().getEnv();
|
||||
if (telemetry.metrics.size > 0) {
|
||||
bootstrapTelemetry(telemetry.microservicesPort);
|
||||
@@ -24,7 +30,7 @@ export async function bootstrap() {
|
||||
|
||||
logger.setContext('Bootstrap');
|
||||
app.useLogger(logger);
|
||||
app.useWebSocketAdapter(new WebSocketAdapter(app));
|
||||
app.useWebSocketAdapter(await createWebSocketAdapter(app));
|
||||
|
||||
await (host ? app.listen(0, host) : app.listen(0));
|
||||
|
||||
|
||||
@@ -233,14 +233,6 @@ export class MediumTestContext<S extends BaseService = BaseService> {
|
||||
return { albumUser: { albumId, userId, role }, result };
|
||||
}
|
||||
|
||||
async softDeleteAsset(assetId: string) {
|
||||
await this.database.updateTable('asset').set({ deletedAt: new Date() }).where('id', '=', assetId).execute();
|
||||
}
|
||||
|
||||
async softDeleteAlbum(albumId: string) {
|
||||
await this.database.updateTable('album').set({ deletedAt: new Date() }).where('id', '=', albumId).execute();
|
||||
}
|
||||
|
||||
async newJobStatus(dto: Partial<Insertable<AssetJobStatusTable>> & { assetId: string }) {
|
||||
const jobStatus = mediumFactory.assetJobStatusInsert({ assetId: dto.assetId });
|
||||
const result = await this.get(AssetRepository).upsertJobStatus(jobStatus);
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import { ClusterMessage, ClusterResponse } from 'socket.io-adapter';
|
||||
import { createBroadcastChannelAdapter } from 'src/middleware/broadcast-channel.adapter';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
const createMockNamespace = () => ({
|
||||
name: '/',
|
||||
sockets: new Map(),
|
||||
adapter: null,
|
||||
server: {
|
||||
encoder: {
|
||||
encode: vi.fn().mockReturnValue([]),
|
||||
},
|
||||
_opts: {},
|
||||
sockets: {
|
||||
sockets: new Map(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('BroadcastChannelAdapter', () => {
|
||||
describe('createBroadcastChannelAdapter', () => {
|
||||
it('should return a factory function', () => {
|
||||
const factory = createBroadcastChannelAdapter();
|
||||
expect(typeof factory).toBe('function');
|
||||
});
|
||||
|
||||
it('should create adapter instance when factory is called', () => {
|
||||
const mockNamespace = createMockNamespace();
|
||||
const factory = createBroadcastChannelAdapter();
|
||||
const adapter = factory(mockNamespace);
|
||||
|
||||
expect(adapter).toBeDefined();
|
||||
expect(adapter.doPublish).toBeDefined();
|
||||
expect(adapter.doPublishResponse).toBeDefined();
|
||||
|
||||
adapter.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BroadcastChannelAdapter message passing', () => {
|
||||
it('should actually send and receive messages between two adapters', async () => {
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
|
||||
const namespace1 = createMockNamespace();
|
||||
const namespace2 = createMockNamespace();
|
||||
|
||||
const adapter1 = factory1(namespace1);
|
||||
const adapter2 = factory2(namespace2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedMessages: ClusterMessage[] = [];
|
||||
const messageReceived = new Promise<void>((resolve) => {
|
||||
const originalOnMessage = adapter2.onMessage.bind(adapter2);
|
||||
adapter2.onMessage = (message: ClusterMessage) => {
|
||||
receivedMessages.push(message);
|
||||
resolve();
|
||||
return originalOnMessage(message);
|
||||
};
|
||||
});
|
||||
|
||||
const testMessage = {
|
||||
type: 2,
|
||||
data: {
|
||||
opts: { rooms: new Set(['room1']) },
|
||||
rooms: ['room1'],
|
||||
},
|
||||
nsp: '/',
|
||||
};
|
||||
|
||||
void adapter1.doPublish(testMessage as any);
|
||||
|
||||
await Promise.race([messageReceived, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||
|
||||
expect(receivedMessages.length).toBeGreaterThan(0);
|
||||
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
});
|
||||
|
||||
it('should send ConfigUpdate-style event and receive it on another adapter', async () => {
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
|
||||
const namespace1 = createMockNamespace();
|
||||
const namespace2 = createMockNamespace();
|
||||
|
||||
const adapter1 = factory1(namespace1);
|
||||
const adapter2 = factory2(namespace2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedMessages: ClusterMessage[] = [];
|
||||
const messageReceived = new Promise<void>((resolve) => {
|
||||
const originalOnMessage = adapter2.onMessage.bind(adapter2);
|
||||
adapter2.onMessage = (message: ClusterMessage) => {
|
||||
receivedMessages.push(message);
|
||||
if ((message as any)?.data?.event === 'ConfigUpdate') {
|
||||
resolve();
|
||||
}
|
||||
return originalOnMessage(message);
|
||||
};
|
||||
});
|
||||
|
||||
const configUpdateMessage = {
|
||||
type: 2,
|
||||
data: {
|
||||
event: 'ConfigUpdate',
|
||||
payload: { newConfig: { ffmpeg: { crf: 23 } }, oldConfig: { ffmpeg: { crf: 20 } } },
|
||||
opts: { rooms: new Set() },
|
||||
rooms: [],
|
||||
},
|
||||
nsp: '/',
|
||||
};
|
||||
|
||||
void adapter1.doPublish(configUpdateMessage as any);
|
||||
|
||||
await Promise.race([messageReceived, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||
|
||||
const configMessages = receivedMessages.filter((m) => (m as any)?.data?.event === 'ConfigUpdate');
|
||||
expect(configMessages.length).toBeGreaterThan(0);
|
||||
expect((configMessages[0] as any).data.payload.newConfig.ffmpeg.crf).toBe(23);
|
||||
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
});
|
||||
|
||||
it('should send AppRestart-style event and receive it on another adapter', async () => {
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
|
||||
const namespace1 = createMockNamespace();
|
||||
const namespace2 = createMockNamespace();
|
||||
|
||||
const adapter1 = factory1(namespace1);
|
||||
const adapter2 = factory2(namespace2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedMessages: ClusterMessage[] = [];
|
||||
const messageReceived = new Promise<void>((resolve) => {
|
||||
const originalOnMessage = adapter2.onMessage.bind(adapter2);
|
||||
adapter2.onMessage = (message: ClusterMessage) => {
|
||||
receivedMessages.push(message);
|
||||
if ((message as any)?.data?.event === 'AppRestart') {
|
||||
resolve();
|
||||
}
|
||||
return originalOnMessage(message);
|
||||
};
|
||||
});
|
||||
|
||||
const appRestartMessage = {
|
||||
type: 2,
|
||||
data: {
|
||||
event: 'AppRestart',
|
||||
payload: { isMaintenanceMode: true },
|
||||
opts: { rooms: new Set() },
|
||||
rooms: [],
|
||||
},
|
||||
nsp: '/',
|
||||
};
|
||||
|
||||
void adapter1.doPublish(appRestartMessage as any);
|
||||
|
||||
await Promise.race([messageReceived, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||
|
||||
const restartMessages = receivedMessages.filter((m) => (m as any)?.data?.event === 'AppRestart');
|
||||
expect(restartMessages.length).toBeGreaterThan(0);
|
||||
expect((restartMessages[0] as any).data.payload.isMaintenanceMode).toBe(true);
|
||||
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
});
|
||||
|
||||
it('should not receive its own messages (echo prevention)', async () => {
|
||||
const factory = createBroadcastChannelAdapter();
|
||||
const namespace = createMockNamespace();
|
||||
const adapter = factory(namespace);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedOwnMessages: ClusterMessage[] = [];
|
||||
const uniqueMarker = `test-${Date.now()}-${Math.random()}`;
|
||||
|
||||
const originalOnMessage = adapter.onMessage.bind(adapter);
|
||||
adapter.onMessage = (message: ClusterMessage) => {
|
||||
if ((message as any)?.data?.marker === uniqueMarker) {
|
||||
receivedOwnMessages.push(message);
|
||||
}
|
||||
return originalOnMessage(message);
|
||||
};
|
||||
|
||||
const testMessage = {
|
||||
type: 2,
|
||||
data: {
|
||||
marker: uniqueMarker,
|
||||
opts: { rooms: new Set() },
|
||||
rooms: [],
|
||||
},
|
||||
nsp: '/',
|
||||
};
|
||||
|
||||
void adapter.doPublish(testMessage as any);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
|
||||
expect(receivedOwnMessages.length).toBe(0);
|
||||
|
||||
adapter.close();
|
||||
});
|
||||
|
||||
it('should send and receive response messages between adapters', async () => {
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
|
||||
const namespace1 = createMockNamespace();
|
||||
const namespace2 = createMockNamespace();
|
||||
|
||||
const adapter1 = factory1(namespace1);
|
||||
const adapter2 = factory2(namespace2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedResponses: ClusterResponse[] = [];
|
||||
const responseReceived = new Promise<void>((resolve) => {
|
||||
const originalOnResponse = adapter1.onResponse.bind(adapter1);
|
||||
adapter1.onResponse = (response: ClusterResponse) => {
|
||||
receivedResponses.push(response);
|
||||
resolve();
|
||||
return originalOnResponse(response);
|
||||
};
|
||||
});
|
||||
|
||||
const responseMessage = {
|
||||
type: 3,
|
||||
data: { result: 'success', count: 42 },
|
||||
};
|
||||
|
||||
void adapter2.doPublishResponse((adapter1 as any).uid, responseMessage as any);
|
||||
|
||||
await Promise.race([responseReceived, new Promise((resolve) => setTimeout(resolve, 500))]);
|
||||
|
||||
expect(receivedResponses.length).toBeGreaterThan(0);
|
||||
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('BroadcastChannelAdapter lifecycle', () => {
|
||||
it('should close cleanly without errors', () => {
|
||||
const factory = createBroadcastChannelAdapter();
|
||||
const namespace = createMockNamespace();
|
||||
const adapter = factory(namespace);
|
||||
|
||||
expect(() => adapter.close()).not.toThrow();
|
||||
});
|
||||
|
||||
it('should handle multiple adapters closing in sequence', () => {
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
const factory3 = createBroadcastChannelAdapter();
|
||||
|
||||
const adapter1 = factory1(createMockNamespace());
|
||||
const adapter2 = factory2(createMockNamespace());
|
||||
const adapter3 = factory3(createMockNamespace());
|
||||
|
||||
expect(() => {
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
adapter3.close();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,159 @@
|
||||
import { Server } from 'socket.io';
|
||||
import { createBroadcastChannelAdapter } from 'src/middleware/broadcast-channel.adapter';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { WebsocketRepository } from 'src/repositories/websocket.repository';
|
||||
import { automock } from 'test/utils';
|
||||
import { vi } from 'vitest';
|
||||
|
||||
describe('WebSocket Integration - serverSend with adapters', () => {
|
||||
describe('BroadcastChannel adapter', () => {
|
||||
it('should broadcast ConfigUpdate event through BroadcastChannel adapter', async () => {
|
||||
const createMockNamespace = () => ({
|
||||
name: '/',
|
||||
sockets: new Map(),
|
||||
adapter: null,
|
||||
server: {
|
||||
encoder: { encode: vi.fn().mockReturnValue([]) },
|
||||
_opts: {},
|
||||
sockets: { sockets: new Map() },
|
||||
},
|
||||
});
|
||||
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
|
||||
const namespace1 = createMockNamespace();
|
||||
const namespace2 = createMockNamespace();
|
||||
|
||||
const adapter1 = factory1(namespace1);
|
||||
const adapter2 = factory2(namespace2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedMessages: any[] = [];
|
||||
vi.spyOn(adapter2, 'onMessage').mockImplementation((message: any) => {
|
||||
receivedMessages.push(message);
|
||||
});
|
||||
|
||||
const configUpdatePayload = {
|
||||
type: 5,
|
||||
data: {
|
||||
event: 'ConfigUpdate',
|
||||
args: [{ newConfig: { ffmpeg: { crf: 23 } }, oldConfig: { ffmpeg: { crf: 20 } } }],
|
||||
},
|
||||
nsp: '/',
|
||||
};
|
||||
|
||||
void adapter1.doPublish(configUpdatePayload as any);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const configMessages = receivedMessages.filter((m) => m?.data?.event === 'ConfigUpdate');
|
||||
expect(configMessages.length).toBeGreaterThan(0);
|
||||
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
});
|
||||
|
||||
it('should broadcast AppRestart event through BroadcastChannel adapter', async () => {
|
||||
const createMockNamespace = () => ({
|
||||
name: '/',
|
||||
sockets: new Map(),
|
||||
adapter: null,
|
||||
server: {
|
||||
encoder: { encode: vi.fn().mockReturnValue([]) },
|
||||
_opts: {},
|
||||
sockets: { sockets: new Map() },
|
||||
},
|
||||
});
|
||||
|
||||
const factory1 = createBroadcastChannelAdapter();
|
||||
const factory2 = createBroadcastChannelAdapter();
|
||||
|
||||
const namespace1 = createMockNamespace();
|
||||
const namespace2 = createMockNamespace();
|
||||
|
||||
const adapter1 = factory1(namespace1);
|
||||
const adapter2 = factory2(namespace2);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const receivedMessages: any[] = [];
|
||||
vi.spyOn(adapter2, 'onMessage').mockImplementation((message: any) => {
|
||||
receivedMessages.push(message);
|
||||
});
|
||||
|
||||
const appRestartPayload = {
|
||||
type: 5,
|
||||
data: {
|
||||
event: 'AppRestart',
|
||||
args: [{ isMaintenanceMode: true }],
|
||||
},
|
||||
nsp: '/',
|
||||
};
|
||||
|
||||
void adapter1.doPublish(appRestartPayload as any);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
const restartMessages = receivedMessages.filter((m) => m?.data?.event === 'AppRestart');
|
||||
expect(restartMessages.length).toBeGreaterThan(0);
|
||||
|
||||
adapter1.close();
|
||||
adapter2.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WebsocketRepository with adapter', () => {
|
||||
it('should call serverSideEmit when serverSend is called', () => {
|
||||
const mockServer = {
|
||||
serverSideEmit: vi.fn(),
|
||||
on: vi.fn(),
|
||||
} as unknown as Server;
|
||||
|
||||
const eventRepository = automock(EventRepository, {
|
||||
args: [undefined, undefined, { setContext: () => {} }],
|
||||
});
|
||||
const loggingRepository = automock(LoggingRepository, {
|
||||
args: [undefined, { getEnv: () => ({ noColor: false }) }],
|
||||
strict: false,
|
||||
});
|
||||
|
||||
const websocketRepository = new WebsocketRepository(eventRepository, loggingRepository);
|
||||
(websocketRepository as any).server = mockServer;
|
||||
|
||||
websocketRepository.serverSend('ConfigUpdate', {
|
||||
newConfig: { ffmpeg: { crf: 23 } } as any,
|
||||
oldConfig: { ffmpeg: { crf: 20 } } as any,
|
||||
});
|
||||
|
||||
expect(mockServer.serverSideEmit).toHaveBeenCalledWith('ConfigUpdate', {
|
||||
newConfig: { ffmpeg: { crf: 23 } },
|
||||
oldConfig: { ffmpeg: { crf: 20 } },
|
||||
});
|
||||
});
|
||||
|
||||
it('should call serverSideEmit for AppRestart event', () => {
|
||||
const mockServer = {
|
||||
serverSideEmit: vi.fn(),
|
||||
on: vi.fn(),
|
||||
} as unknown as Server;
|
||||
|
||||
const eventRepository = automock(EventRepository, {
|
||||
args: [undefined, undefined, { setContext: () => {} }],
|
||||
});
|
||||
const loggingRepository = automock(LoggingRepository, {
|
||||
args: [undefined, { getEnv: () => ({ noColor: false }) }],
|
||||
strict: false,
|
||||
});
|
||||
|
||||
const websocketRepository = new WebsocketRepository(eventRepository, loggingRepository);
|
||||
(websocketRepository as any).server = mockServer;
|
||||
|
||||
websocketRepository.serverSend('AppRestart', { isMaintenanceMode: true });
|
||||
|
||||
expect(mockServer.serverSideEmit).toHaveBeenCalledWith('AppRestart', { isMaintenanceMode: true });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||
import { SocketIoAdapter } from 'src/enum';
|
||||
import { asPgPoolSsl, createWebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { Mocked, vi } from 'vitest';
|
||||
|
||||
describe('asPgPoolSsl', () => {
|
||||
it('should return false for undefined ssl', () => {
|
||||
expect(asPgPoolSsl()).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for ssl = false', () => {
|
||||
expect(asPgPoolSsl(false)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for ssl = "allow"', () => {
|
||||
expect(asPgPoolSsl('allow')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return { rejectUnauthorized: false } for ssl = true', () => {
|
||||
expect(asPgPoolSsl(true)).toEqual({ rejectUnauthorized: false });
|
||||
});
|
||||
|
||||
it('should return { rejectUnauthorized: false } for ssl = "prefer"', () => {
|
||||
expect(asPgPoolSsl('prefer')).toEqual({ rejectUnauthorized: false });
|
||||
});
|
||||
|
||||
it('should return { rejectUnauthorized: false } for ssl = "require"', () => {
|
||||
expect(asPgPoolSsl('require')).toEqual({ rejectUnauthorized: false });
|
||||
});
|
||||
|
||||
it('should return { rejectUnauthorized: true } for ssl = "verify-full"', () => {
|
||||
expect(asPgPoolSsl('verify-full')).toEqual({ rejectUnauthorized: true });
|
||||
});
|
||||
|
||||
it('should pass through object ssl config unchanged', () => {
|
||||
const sslConfig = { ca: 'certificate', rejectUnauthorized: true };
|
||||
expect(asPgPoolSsl(sslConfig)).toBe(sslConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createWebSocketAdapter', () => {
|
||||
let mockApp: Mocked<INestApplication>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockApp = {
|
||||
getHttpServer: vi.fn().mockReturnValue({}),
|
||||
} as unknown as Mocked<INestApplication>;
|
||||
});
|
||||
|
||||
describe('BroadcastChannel adapter', () => {
|
||||
it('should create BroadcastChannel adapter when configured', async () => {
|
||||
const adapter = await createWebSocketAdapter(mockApp, SocketIoAdapter.BroadcastChannel);
|
||||
|
||||
expect(adapter).toBeDefined();
|
||||
expect(adapter).toBeInstanceOf(IoAdapter);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Postgres adapter', () => {
|
||||
it('should create Postgres adapter when configured', async () => {
|
||||
const adapter = await createWebSocketAdapter(mockApp, SocketIoAdapter.Postgres);
|
||||
|
||||
expect(adapter).toBeDefined();
|
||||
expect(adapter).toBeInstanceOf(IoAdapter);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -95,469 +95,6 @@ describe(SharedLinkService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should return all shared links even when they share the same createdAt', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
const sameTimestamp = '2024-01-01T00:00:00.000Z';
|
||||
|
||||
const link1 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: sameTimestamp,
|
||||
});
|
||||
|
||||
const link2 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: sameTimestamp,
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(2);
|
||||
const ids = result.map((r) => r.id);
|
||||
expect(ids).toContain(link1.id);
|
||||
expect(ids).toContain(link2.id);
|
||||
});
|
||||
|
||||
it('should return shared links sorted by createdAt in descending order', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
const link1 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: '2021-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const link2 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: '2023-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const link3 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
createdAt: '2022-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((r) => r.id)).toEqual([link2.id, link3.id, link1.id]);
|
||||
});
|
||||
|
||||
it('should not return shared links belonging to other users', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
|
||||
const { user: userA } = await ctx.newUser();
|
||||
const { user: userB } = await ctx.newUser();
|
||||
const authA = factory.auth({ user: userA });
|
||||
const authB = factory.auth({ user: userB });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
const linkA = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: userA.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
});
|
||||
|
||||
await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: userB.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
});
|
||||
|
||||
const resultA = await sut.getAll(authA, {});
|
||||
expect(resultA).toHaveLength(1);
|
||||
expect(resultA[0].id).toBe(linkA.id);
|
||||
|
||||
const resultB = await sut.getAll(authB, {});
|
||||
expect(resultB).toHaveLength(1);
|
||||
expect(resultB[0].id).not.toBe(linkA.id);
|
||||
});
|
||||
|
||||
it('should filter by albumId', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user.id });
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
const link1 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album1.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album2.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, { albumId: album1.id });
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toBe(link1.id);
|
||||
});
|
||||
|
||||
it('should return album shared links with album data', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].album).toBeDefined();
|
||||
expect(result[0].album!.id).toBe(album.id);
|
||||
});
|
||||
|
||||
it('should return multiple album shared links without sql error from json group by', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { album: album1 } = await ctx.newAlbum({ ownerId: user.id });
|
||||
const { album: album2 } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
const link1 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album1.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
const link2 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album2.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(2);
|
||||
const ids = result.map((r) => r.id);
|
||||
expect(ids).toContain(link1.id);
|
||||
expect(ids).toContain(link2.id);
|
||||
expect(result[0].album).toBeDefined();
|
||||
expect(result[1].album).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return mixed album and individual shared links together', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
const albumLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
const albumLink2 = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
const individualLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(3);
|
||||
const ids = result.map((r) => r.id);
|
||||
expect(ids).toContain(albumLink.id);
|
||||
expect(ids).toContain(albumLink2.id);
|
||||
expect(ids).toContain(individualLink.id);
|
||||
});
|
||||
|
||||
it('should return only the first asset as cover for an individual shared link', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const assets = await Promise.all([
|
||||
ctx.newAsset({ ownerId: user.id, fileCreatedAt: '2021-01-01T00:00:00.000Z' }),
|
||||
ctx.newAsset({ ownerId: user.id, fileCreatedAt: '2023-01-01T00:00:00.000Z' }),
|
||||
ctx.newAsset({ ownerId: user.id, fileCreatedAt: '2022-01-01T00:00:00.000Z' }),
|
||||
]);
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
|
||||
await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: assets.map(({ asset }) => asset.id),
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].assets).toHaveLength(1);
|
||||
expect(result[0].assets[0].id).toBe(assets[0].asset.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should not return trashed assets for an individual shared link', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { asset: visibleAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: visibleAsset.id, make: 'Canon' });
|
||||
|
||||
const { asset: trashedAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: trashedAsset.id, make: 'Canon' });
|
||||
await ctx.softDeleteAsset(trashedAsset.id);
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
const sharedLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [visibleAsset.id, trashedAsset.id],
|
||||
});
|
||||
|
||||
const result = await sut.get(auth, sharedLink.id);
|
||||
expect(result).toBeDefined();
|
||||
expect(result!.assets).toHaveLength(1);
|
||||
expect(result!.assets[0].id).toBe(visibleAsset.id);
|
||||
});
|
||||
|
||||
it('should return empty assets when all individually shared assets are trashed', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
await ctx.softDeleteAsset(asset.id);
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
const sharedLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
|
||||
await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({
|
||||
assets: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return trashed assets in a shared album', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const { asset: visibleAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: visibleAsset.id, make: 'Canon' });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: visibleAsset.id });
|
||||
|
||||
const { asset: trashedAsset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: trashedAsset.id, make: 'Canon' });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: trashedAsset.id });
|
||||
await ctx.softDeleteAsset(trashedAsset.id);
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
const sharedLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: true,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({
|
||||
album: expect.objectContaining({ assetCount: 1 }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should return an empty asset count when all album assets are trashed', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||
await ctx.newExif({ assetId: asset.id, make: 'Canon' });
|
||||
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
|
||||
await ctx.softDeleteAsset(asset.id);
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
const sharedLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
await expect(sut.get(auth, sharedLink.id)).resolves.toMatchObject({
|
||||
album: expect.objectContaining({ assetCount: 0 }),
|
||||
});
|
||||
});
|
||||
|
||||
it('should not return an album shared link when the album is trashed', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
const sharedLink = await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
await ctx.softDeleteAlbum(album.id);
|
||||
|
||||
await expect(sut.get(auth, sharedLink.id)).rejects.toThrow('Shared link not found');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should not return trashed assets as cover for an individual shared link', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
|
||||
const { asset: trashedAsset } = await ctx.newAsset({
|
||||
ownerId: user.id,
|
||||
fileCreatedAt: '2020-01-01T00:00:00.000Z',
|
||||
});
|
||||
await ctx.softDeleteAsset(trashedAsset.id);
|
||||
|
||||
const { asset: visibleAsset } = await ctx.newAsset({
|
||||
ownerId: user.id,
|
||||
fileCreatedAt: '2021-01-01T00:00:00.000Z',
|
||||
});
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Individual,
|
||||
assetIds: [trashedAsset.id, visibleAsset.id],
|
||||
});
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].assets).toHaveLength(1);
|
||||
expect(result[0].assets[0].id).toBe(visibleAsset.id);
|
||||
});
|
||||
|
||||
it('should not return an album shared link when the album is trashed', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
const auth = factory.auth({ user });
|
||||
const { album } = await ctx.newAlbum({ ownerId: user.id });
|
||||
|
||||
const sharedLinkRepo = ctx.get(SharedLinkRepository);
|
||||
await sharedLinkRepo.create({
|
||||
key: randomBytes(16),
|
||||
id: factory.uuid(),
|
||||
userId: user.id,
|
||||
albumId: album.id,
|
||||
allowUpload: false,
|
||||
type: SharedLinkType.Album,
|
||||
});
|
||||
|
||||
await ctx.softDeleteAlbum(album.id);
|
||||
|
||||
const result = await sut.getAll(auth, {});
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove individually shared asset', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
|
||||
getForOriginal: vitest.fn(),
|
||||
getForOriginals: vitest.fn(),
|
||||
getForThumbnail: vitest.fn(),
|
||||
getForTiles: vitest.fn(),
|
||||
getForVideo: vitest.fn(),
|
||||
getForEdit: vitest.fn(),
|
||||
getForOcr: vitest.fn(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseExtension, ImmichEnvironment, ImmichWorker, LogFormat } from 'src/enum';
|
||||
import { DatabaseExtension, ImmichEnvironment, ImmichWorker, LogFormat, SocketIoAdapter } from 'src/enum';
|
||||
import { ConfigRepository, EnvData } from 'src/repositories/config.repository';
|
||||
import { RepositoryInterface } from 'src/types';
|
||||
import { Mocked, vitest } from 'vitest';
|
||||
@@ -99,6 +99,10 @@ const envData: EnvData = {
|
||||
},
|
||||
},
|
||||
|
||||
socketIo: {
|
||||
adapter: SocketIoAdapter.Postgres,
|
||||
},
|
||||
|
||||
noColor: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Mocked, vitest } from 'vitest';
|
||||
export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaRepository>> => {
|
||||
return {
|
||||
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateTiles: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
writeExif: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
copyTagGroup: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
"@photo-sphere-viewer/equirectangular-tiles-adapter": "^5.14.1",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
|
||||
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
|
||||
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
||||
|
||||
BIN
web/src/lib/assets/apple/apple-splash-1125-2436.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1136-640.png
Normal file
|
After Width: | Height: | Size: 61 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1170-2532.png
Normal file
|
After Width: | Height: | Size: 156 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1179-2556.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1242-2208.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1242-2688.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1284-2778.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1290-2796.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1334-750.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1536-2048.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1620-2160.png
Normal file
|
After Width: | Height: | Size: 228 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1668-2224.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1668-2388.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
web/src/lib/assets/apple/apple-splash-1792-828.png
Normal file
|
After Width: | Height: | Size: 74 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2048-1536.png
Normal file
|
After Width: | Height: | Size: 202 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2048-2732.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2160-1620.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2208-1242.png
Normal file
|
After Width: | Height: | Size: 137 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2224-1668.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2388-1668.png
Normal file
|
After Width: | Height: | Size: 220 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2436-1125.png
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2532-1170.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2556-1179.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2688-1242.png
Normal file
|
After Width: | Height: | Size: 122 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2732-2048.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2778-1284.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
web/src/lib/assets/apple/apple-splash-2796-1290.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
web/src/lib/assets/apple/apple-splash-640-1136.png
Normal file
|
After Width: | Height: | Size: 73 KiB |
BIN
web/src/lib/assets/apple/apple-splash-750-1334.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
web/src/lib/assets/apple/apple-splash-828-1792.png
Normal file
|
After Width: | Height: | Size: 98 KiB |