Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Jogenfors
7eac0847f6 feat: add checksum algorithm field 2026-02-27 15:40:56 +01:00
34 changed files with 152 additions and 465 deletions

View File

@@ -689,7 +689,6 @@
"backup_settings_subtitle": "Manage upload settings",
"backup_upload_details_page_more_details": "Tap for more details",
"backward": "Backward",
"battery_optimization_backup_reliability": "Disabling battery optimizations can improve the reliability of background backup",
"biometric_auth_enabled": "Biometric authentication enabled",
"biometric_locked_out": "You are locked out of biometric authentication",
"biometric_no_options": "No biometric options available",
@@ -1624,7 +1623,6 @@
"not_selected": "Not selected",
"notes": "Notes",
"nothing_here_yet": "Nothing here yet",
"notification_backup_reliability": "Enable notifications to improve background backup reliability",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",

View File

@@ -1 +0,0 @@
3.13

View File

@@ -48,14 +48,14 @@ FROM python:3.13-slim-trixie@sha256:3de9a8d7aedbb7984dc18f2dff178a7850f16c1ae7c3
RUN apt-get update && \
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-core-2_2.28.4+20760_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.28.4/intel-igc-opencl-2_2.28.4+20760_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/intel-opencl-icd_26.05.37020.3-0_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-core-2_2.27.10+20617_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/v2.27.10/intel-igc-opencl-2_2.27.10+20617_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/intel-opencl-icd_26.01.36711.4-0_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-core_1.0.17537.24_amd64.deb && \
wget -nv https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17537.24/intel-igc-opencl_1.0.17537.24_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/24.35.30872.36/intel-opencl-icd-legacy1_24.35.30872.36_amd64.deb && \
# TODO: Figure out how to get renovate to manage this differently versioned libigdgmm file
wget -nv https://github.com/intel/compute-runtime/releases/download/26.05.37020.3/libigdgmm12_22.9.0_amd64.deb && \
wget -nv https://github.com/intel/compute-runtime/releases/download/26.01.36711.4/libigdgmm12_22.9.0_amd64.deb && \
dpkg -i *.deb && \
rm *.deb && \
apt-get remove wget -yqq && \

View File

@@ -49,7 +49,7 @@ dev = ["locust>=2.15.1", { include-group = "test" }, { include-group = "lint" }]
[project.optional-dependencies]
cpu = ["onnxruntime>=1.23.2,<2"]
cuda = ["onnxruntime-gpu>=1.23.2,<2"]
openvino = ["onnxruntime-openvino>=1.24.1,<2"]
openvino = ["onnxruntime-openvino>=1.23.0,<2"]
armnn = ["onnxruntime>=1.23.2,<2"]
rknn = ["onnxruntime>=1.23.2,<2", "rknn-toolkit-lite2>=2.3.0,<3"]
rocm = ["onnxruntime-migraphx>=1.23.2,<2"]

View File

@@ -262,6 +262,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "humanfriendly" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520, upload-time = "2021-06-11T10:22:45.202Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" },
]
[[package]]
name = "colorlog"
version = "6.9.0"
@@ -874,6 +886,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
]
[[package]]
name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702, upload-time = "2021-09-17T21:40:43.31Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794, upload-time = "2021-09-17T21:40:39.897Z" },
]
[[package]]
name = "idna"
version = "3.11"
@@ -993,7 +1017,7 @@ requires-dist = [
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime-migraphx", marker = "extra == 'rocm'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.23.0,<2" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" },
{ name = "pillow", specifier = ">=12.1.1,<12.2" },
@@ -1724,9 +1748,10 @@ wheels = [
[[package]]
name = "onnxruntime-openvino"
version = "1.24.1"
version = "1.23.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
@@ -1734,12 +1759,12 @@ dependencies = [
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/99/16/69ca742f0b65c40d4de3ff44bb6abc23c47b23e932bc901116176ae69922/onnxruntime_openvino-1.24.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3007c803634cc69c6d52af1dea7ce729d9bb62b9a11070fd2f959119199007a8", size = 84430935, upload-time = "2026-02-26T13:44:32.193Z" },
{ url = "https://files.pythonhosted.org/packages/aa/73/619bb416bbfc40aebdd493fd6800d2637359294fe683d8a6bae3ff8d869a/onnxruntime_openvino-1.24.1-cp311-cp311-win_amd64.whl", hash = "sha256:8042698232bf67f1f6b219c2b07728d7ae7ddff17d8524588de3675480609aef", size = 13655357, upload-time = "2026-02-26T13:44:35.555Z" },
{ url = "https://files.pythonhosted.org/packages/50/cf/17ba72de2df0fcba349937d2788f154397bbc2d1a2d67772a97e26f6bc5f/onnxruntime_openvino-1.24.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:d617fac2f59a6ab5ea59a788c3e1592240a129642519aaeaa774761dfe35150e", size = 84433207, upload-time = "2026-02-26T13:44:41.395Z" },
{ url = "https://files.pythonhosted.org/packages/59/37/d301f2c68b19a9485ed5db3047e0fb52478f3e73eb08c7d2a7c61be7cc1c/onnxruntime_openvino-1.24.1-cp312-cp312-win_amd64.whl", hash = "sha256:f186335a9c9b255633275290da7521d3d4d14c7773fee3127bfa040234d3fa5a", size = 13658075, upload-time = "2026-02-26T13:44:44.905Z" },
{ url = "https://files.pythonhosted.org/packages/08/07/f225999919f56506b603aaa3ff837ad563ab26f86906ed7fa7e5abcd849e/onnxruntime_openvino-1.24.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c3bb73e68ac27f4891af8a595c1faf574ec68b772e6583c90a0b997a1822782", size = 84433183, upload-time = "2026-02-26T13:44:50.254Z" },
{ url = "https://files.pythonhosted.org/packages/3e/92/46ae2cd565961a89189900f385bb2f13a9fa731ea4674001d23720fbb1e0/onnxruntime_openvino-1.24.1-cp313-cp313-win_amd64.whl", hash = "sha256:434bf49aa71393c577a456c9d76c98e6d6958a833fa0876793e3d5437b5a511a", size = 13658485, upload-time = "2026-02-26T13:44:53.889Z" },
{ url = "https://files.pythonhosted.org/packages/5a/10/adcd4ac68ffc8dee003553125ef5c091be822e2d7c1077d0bb85690baa9c/onnxruntime_openvino-1.23.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:91938837e6e92e30c63d12fad68a8a4959c40d2eade2bd60f38bdd5b6392f8d3", size = 70481480, upload-time = "2025-10-14T15:19:45.882Z" },
{ url = "https://files.pythonhosted.org/packages/97/95/25f28d6fecf300aa0af393e96af9e00cc676e5dab650ab84f2122610df50/onnxruntime_openvino-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f05d2d6a804fb70d3f4329d777ac62439773dcc2df827dd5f42644b10bf1fea", size = 13117353, upload-time = "2025-10-14T15:19:49.014Z" },
{ url = "https://files.pythonhosted.org/packages/42/0c/8d97419dfeedf419c5fe5293f3dbc59284855a63ad22e71f46c0010c9dc4/onnxruntime_openvino-1.23.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b963ea19bf9856f3d6b2f719d451f2eeae482a8f69c729906465aa4f27f4d39c", size = 70483359, upload-time = "2025-10-14T15:19:52.88Z" },
{ url = "https://files.pythonhosted.org/packages/29/30/ff6111b16ffb4187c462824aa4e95acc20fdd90f856d44a339d56c6dacd6/onnxruntime_openvino-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:937e52657f94c56990a6e5bd4c3705bd6e970834c7c94e23d300dde6848f2889", size = 13117933, upload-time = "2025-10-14T15:19:58.319Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/e42f618a8ec5fcf825fed4fdc8125f7105256cc6020b84567ecb88d5e2b7/onnxruntime_openvino-1.23.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2e93b9a8323e196b7433866054a59260f2206ab6fb0e7223dda91da71f1db8c5", size = 70483088, upload-time = "2025-10-14T15:20:02.425Z" },
{ url = "https://files.pythonhosted.org/packages/4a/f9/a531dc497dc113dc14df9a9de5aacb1676cadebc3ec6cc7cd3ca65cb3db0/onnxruntime_openvino-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:0ebbf70929de4ce269371cb255536bbedef588932d744da0b40e66c38a620f35", size = 13118206, upload-time = "2025-10-14T15:20:05.587Z" },
]
[[package]]
@@ -2179,6 +2204,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/92/8486ede85fcc088f1b3dba4ce92dd29d126fd96b0008ea213167940a2475/pyparsing-3.1.1-py3-none-any.whl", hash = "sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb", size = 103139, upload-time = "2023-07-30T15:06:59.829Z" },
]
[[package]]
name = "pyreadline3"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/86/3d61a61f36a0067874a00cb4dceb9028d34b6060e47828f7fc86fb9f7ee9/pyreadline3-3.4.1.tar.gz", hash = "sha256:6f3d1f7b8a31ba32b73917cefc1f28cc660562f39aea8646d30bd6eff21f7bae", size = 86465, upload-time = "2022-01-24T20:05:11.66Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/fc/a3c13ded7b3057680c8ae95a9b6cc83e63657c38e0005c400a5d018a33a7/pyreadline3-3.4.1-py3-none-any.whl", hash = "sha256:b0efb6516fd4fb07b45949053826a62fa4cb353db5be2bbb4a7aa1fdd1e345fb", size = 95203, upload-time = "2022-01-24T20:05:10.442Z" },
]
[[package]]
name = "pytest"
version = "9.0.2"

View File

@@ -16,8 +16,6 @@ import app.alextran.immich.images.LocalImageApi
import app.alextran.immich.images.LocalImagesImpl
import app.alextran.immich.images.RemoteImageApi
import app.alextran.immich.images.RemoteImagesImpl
import app.alextran.immich.permission.PermissionApi
import app.alextran.immich.permission.PermissionApiImpl
import app.alextran.immich.sync.NativeSyncApi
import app.alextran.immich.sync.NativeSyncApiImpl26
import app.alextran.immich.sync.NativeSyncApiImpl30
@@ -50,7 +48,6 @@ class MainActivity : FlutterFragmentActivity() {
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
PermissionApi.setUp(messenger, PermissionApiImpl(ctx))
flutterEngine.plugins.add(BackgroundServicePlugin())
flutterEngine.plugins.add(backgroundEngineLockImpl)

View File

@@ -1,114 +0,0 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
package app.alextran.immich.permission
import android.util.Log
import io.flutter.plugin.common.BasicMessageChannel
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MessageCodec
import io.flutter.plugin.common.StandardMethodCodec
import io.flutter.plugin.common.StandardMessageCodec
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
private object PermissionApiPigeonUtils {
fun wrapResult(result: Any?): List<Any?> {
return listOf(result)
}
fun wrapError(exception: Throwable): List<Any?> {
return if (exception is FlutterError) {
listOf(
exception.code,
exception.message,
exception.details
)
} else {
listOf(
exception.javaClass.simpleName,
exception.toString(),
"Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
)
}
}
}
/**
* Error class for passing custom error details to Flutter via a thrown PlatformException.
* @property code The error code.
* @property message The error message.
* @property details The error details. Must be a datatype supported by the api codec.
*/
class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
enum class PermissionStatus(val raw: Int) {
GRANTED(0),
DENIED(1),
PERMANENTLY_DENIED(2);
companion object {
fun ofRaw(raw: Int): PermissionStatus? {
return values().firstOrNull { it.raw == raw }
}
}
}
private open class PermissionApiPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return when (type) {
129.toByte() -> {
return (readValue(buffer) as Long?)?.let {
PermissionStatus.ofRaw(it.toInt())
}
}
else -> super.readValueOfType(type, buffer)
}
}
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
when (value) {
is PermissionStatus -> {
stream.write(129)
writeValue(stream, value.raw)
}
else -> super.writeValue(stream, value)
}
}
}
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface PermissionApi {
fun isIgnoringBatteryOptimizations(): PermissionStatus
companion object {
/** The codec used by PermissionApi. */
val codec: MessageCodec<Any?> by lazy {
PermissionApiPigeonCodec()
}
/** Sets up an instance of `PermissionApi` to handle messages through the `binaryMessenger`. */
@JvmOverloads
fun setUp(binaryMessenger: BinaryMessenger, api: PermissionApi?, messageChannelSuffix: String = "") {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.isIgnoringBatteryOptimizations())
} catch (exception: Throwable) {
PermissionApiPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}

View File

@@ -1,19 +0,0 @@
package app.alextran.immich.permission
import android.content.Context
import android.os.PowerManager
class PermissionApiImpl(context: Context) : PermissionApi {
private val ctx: Context = context.applicationContext
private val powerManager =
ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
override fun isIgnoringBatteryOptimizations(): PermissionStatus {
if (powerManager.isIgnoringBatteryOptimizations(ctx.packageName)) {
return PermissionStatus.GRANTED
}
return PermissionStatus.DENIED
}
}

View File

@@ -8,25 +8,18 @@ import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_mobile/platform/permission_api.g.dart';
import 'package:immich_mobile/presentation/widgets/backup/backup_toggle_button.widget.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:immich_mobile/providers/notification_permission.provider.dart';
import 'package:immich_mobile/providers/sync_status.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart' as pm;
import 'package:url_launcher/url_launcher.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@RoutePage()
@@ -168,7 +161,11 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
),
),
},
const _BackupFooter(),
TextButton.icon(
icon: const Icon(Icons.info_outline_rounded),
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
label: Text("view_details".t(context: context)),
),
],
],
),
@@ -179,130 +176,6 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
}
}
class _BackupFooter extends ConsumerStatefulWidget {
const _BackupFooter();
@override
ConsumerState<_BackupFooter> createState() => _BackupFooterState();
}
class _BackupFooterState extends ConsumerState<_BackupFooter> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (CurrentPlatform.isAndroid && state == AppLifecycleState.resumed && mounted) {
unawaited(ref.read(notificationPermissionProvider.notifier).getNotificationPermission());
unawaited(ref.read(_batteryOptimizationProvider.notifier).getBatteryOptimizationPermission());
}
}
void showPermissionsDialog() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
content: Text(context.t.notification_permission_dialog_content),
actions: [
TextButton(child: Text(context.t.cancel), onPressed: () => ctx.pop()),
TextButton(
onPressed: () {
context.pop();
pm.openAppSettings();
},
child: Text(context.t.settings),
),
],
),
);
}
void showBatteryOptimizationInfo() {
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (BuildContext ctx) {
return AlertDialog(
title: Text(context.t.backup_controller_page_background_battery_info_title),
content: SingleChildScrollView(child: Text(context.t.backup_controller_page_background_battery_info_message)),
actions: [
ElevatedButton(
onPressed: () => launchUrl(Uri.parse('https://dontkillmyapp.com'), mode: LaunchMode.externalApplication),
child: Text(
context.t.backup_controller_page_background_battery_info_link,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
),
ElevatedButton(
child: Text(
context.t.backup_controller_page_background_battery_info_ok,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
),
onPressed: () => ctx.pop(),
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
final isBackupEnabled = ref.watch(_backupStatusProvider).valueOrNull ?? false;
final notificationStatus = ref.watch(notificationPermissionProvider);
final batteryOptimizationStatus = ref.watch(_batteryOptimizationProvider).valueOrNull;
return Column(
children: [
if (CurrentPlatform.isAndroid && isBackupEnabled) ...[
if (notificationStatus != pm.PermissionStatus.granted)
TextButton.icon(
icon: Icon(Icons.open_in_new_outlined, color: context.colorScheme.onSurfaceSecondary),
label: Text(
context.t.notification_backup_reliability,
textAlign: TextAlign.center,
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
onPressed: () {
ref.read(notificationPermissionProvider.notifier).requestNotificationPermission().then((p) {
if (p == pm.PermissionStatus.permanentlyDenied) {
showPermissionsDialog();
}
});
},
),
// Show only after notification permission is granted
if (notificationStatus == pm.PermissionStatus.granted &&
batteryOptimizationStatus != pm.PermissionStatus.granted)
TextButton.icon(
icon: Icon(Icons.open_in_new_outlined, color: context.colorScheme.onSurfaceSecondary),
label: Text(
context.t.battery_optimization_backup_reliability,
textAlign: TextAlign.center,
style: context.textTheme.bodySmall?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
onPressed: showBatteryOptimizationInfo,
),
],
TextButton.icon(
icon: const Icon(Icons.info_outline_rounded),
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
label: Text(context.t.view_details),
),
],
);
}
}
class _BackupAlbumSelectionCard extends ConsumerWidget {
const _BackupAlbumSelectionCard();
@@ -648,28 +521,3 @@ class _PreparingStatusState extends ConsumerState {
);
}
}
final _backupStatusProvider = StreamProvider.autoDispose<bool?>((ref) async* {
yield* ref.watch(storeServiceProvider).watch(StoreKey.enableBackup);
});
final _batteryOptimizationProvider = AsyncNotifierProvider<_BatteryOptimizationNotifier, pm.PermissionStatus>(
_BatteryOptimizationNotifier.new,
);
class _BatteryOptimizationNotifier extends AsyncNotifier<pm.PermissionStatus> {
Future<pm.PermissionStatus> getBatteryOptimizationPermission() async {
final pm.PermissionStatus status;
final isIgnoring = await ref.read(permissionApiProvider).isIgnoringBatteryOptimizations();
if (isIgnoring == PermissionStatus.granted) {
status = pm.PermissionStatus.granted;
} else {
status = pm.PermissionStatus.denied;
}
state = AsyncValue.data(status);
return status;
}
@override
FutureOr<pm.PermissionStatus> build() => getBatteryOptimizationPermission();
}

View File

@@ -1,87 +0,0 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';
PlatformException _createConnectionError(String channelName) {
return PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
}
enum PermissionStatus { granted, denied, permanentlyDenied }
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is PermissionStatus) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final int? value = readValue(buffer) as int?;
return value == null ? null : PermissionStatus.values[value];
default:
return super.readValueOfType(type, buffer);
}
}
}
class PermissionApi {
/// Constructor for [PermissionApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
PermissionApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
Future<PermissionStatus> isIgnoringBatteryOptimizations() async {
final String pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.PermissionApi.isIgnoringBatteryOptimizations$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final List<Object?>? pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else if (pigeonVar_replyList[0] == null) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
} else {
return (pigeonVar_replyList[0] as PermissionStatus?)!;
}
}
}

View File

@@ -8,7 +8,6 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
@@ -26,15 +25,6 @@ class DeletePermanentActionButton extends ConsumerWidget {
return;
}
final count = source == ActionSource.viewer ? 1 : ref.read(multiSelectProvider).selectedAssets.length;
final confirm =
await showDialog<bool>(
context: context,
builder: (context) => PermanentDeleteDialog(count: count),
) ??
false;
if (!confirm) return;
final result = await ref.read(actionProvider.notifier).deleteRemoteAndLocal(source);
ref.read(multiSelectProvider.notifier).reset();

View File

@@ -5,7 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/permanent_delete_dialog.dart';
import 'package:immich_mobile/widgets/asset_grid/trash_delete_dialog.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
/// This delete action has the following behavior:
@@ -28,7 +28,7 @@ class DeleteTrashActionButton extends ConsumerWidget {
final confirmDelete =
await showDialog<bool>(
context: context,
builder: (context) => PermanentDeleteDialog(count: selectCount),
builder: (context) => TrashDeleteDialog(count: selectCount),
) ??
false;
if (!confirmDelete) {

View File

@@ -3,10 +3,9 @@ import 'package:immich_mobile/domain/services/background_worker.service.dart';
import 'package:immich_mobile/platform/background_worker_api.g.dart';
import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/platform/connectivity_api.g.dart';
import 'package:immich_mobile/platform/local_image_api.g.dart';
import 'package:immich_mobile/platform/native_sync_api.g.dart';
import 'package:immich_mobile/platform/local_image_api.g.dart';
import 'package:immich_mobile/platform/network_api.g.dart';
import 'package:immich_mobile/platform/permission_api.g.dart';
import 'package:immich_mobile/platform/remote_image_api.g.dart';
final backgroundWorkerFgServiceProvider = Provider((_) => BackgroundWorkerFgService(BackgroundWorkerFgHostApi()));
@@ -19,8 +18,6 @@ final nativeSyncApiProvider = Provider<NativeSyncApi>((_) => NativeSyncApi());
final connectivityApiProvider = Provider<ConnectivityApi>((_) => ConnectivityApi());
final permissionApiProvider = Provider<PermissionApi>((_) => PermissionApi());
final localImageApi = LocalImageApi();
final remoteImageApi = RemoteImageApi();

View File

@@ -3,8 +3,8 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/generated/translations.g.dart';
import 'package:immich_ui/immich_ui.dart';
class PermanentDeleteDialog extends StatelessWidget {
const PermanentDeleteDialog({super.key, required this.count});
class TrashDeleteDialog extends StatelessWidget {
const TrashDeleteDialog({super.key, required this.count});
final int count;

View File

@@ -13,7 +13,6 @@ pigeon:
dart run pigeon --input pigeon/background_worker_lock_api.dart
dart run pigeon --input pigeon/connectivity_api.dart
dart run pigeon --input pigeon/network_api.dart
dart run pigeon --input pigeon/permission_api.dart
dart format lib/platform/native_sync_api.g.dart
dart format lib/platform/local_image_api.g.dart
dart format lib/platform/remote_image_api.g.dart
@@ -21,7 +20,6 @@ pigeon:
dart format lib/platform/background_worker_lock_api.g.dart
dart format lib/platform/connectivity_api.g.dart
dart format lib/platform/network_api.g.dart
dart format lib/platform/permission_api.g.dart
watch:
dart run build_runner watch --delete-conflicting-outputs

View File

@@ -40,13 +40,7 @@ depends = [
[tasks."codegen:translation"]
alias = "translation"
description = "Generate translations from i18n JSONs"
run = [
{ task = "//:i18n:format-fix" },
{ tasks = [
"i18n:loader",
"i18n:keys",
] },
]
run = [{ task = "//:i18n:format-fix" }, { tasks = ["i18n:loader", "i18n:keys"] }]
[tasks."codegen:app-icon"]
description = "Generate app icons"
@@ -152,19 +146,6 @@ run = [
"dart format lib/platform/connectivity_api.g.dart",
]
[tasks."pigeon:permission"]
description = "Generate permission API pigeon code"
hide = true
sources = ["pigeon/permission_api.dart"]
outputs = [
"lib/platform/permission_api.g.dart",
"android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt",
]
run = [
"dart run pigeon --input pigeon/permission_api.dart",
"dart format lib/platform/permission_api.g.dart",
]
[tasks."i18n:loader"]
description = "Generate i18n loader"
hide = true

View File

@@ -1,17 +0,0 @@
import 'package:pigeon/pigeon.dart';
enum PermissionStatus { granted, denied, permanentlyDenied }
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/platform/permission_api.g.dart',
kotlinOut: 'android/app/src/main/kotlin/app/alextran/immich/permission/PermissionApi.g.kt',
kotlinOptions: KotlinOptions(package: 'app.alextran.immich.permission'),
dartOptions: DartOptions(),
dartPackageName: 'immich_mobile',
),
)
@HostApi()
abstract class PermissionApi {
PermissionStatus isIgnoringBatteryOptimizations();
}

View File

@@ -5,6 +5,7 @@ import {
AssetFileType,
AssetType,
AssetVisibility,
ChecksumAlgorithm,
MemoryType,
Permission,
PluginContext,
@@ -111,6 +112,7 @@ export type Memory = {
export type Asset = {
id: string;
checksum: Buffer<ArrayBufferLike>;
checksumAlgorithm: ChecksumAlgorithm;
deviceAssetId: string;
deviceId: string;
fileCreatedAt: Date;
@@ -329,6 +331,7 @@ export const columns = {
asset: [
'asset.id',
'asset.checksum',
'asset.checksumAlgorithm',
'asset.deviceAssetId',
'asset.deviceId',
'asset.fileCreatedAt',

View File

@@ -13,7 +13,7 @@ import {
} from 'src/dtos/person.dto';
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
import { ImageDimensions } from 'src/types';
import { getDimensions } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
@@ -147,6 +147,7 @@ export type MapAsset = {
updateId: string;
status: AssetStatus;
checksum: Buffer<ArrayBufferLike>;
checksumAlgorithm: ChecksumAlgorithm;
deviceAssetId: string;
deviceId: string;
duplicateId: string | null;

View File

@@ -37,6 +37,11 @@ export enum AssetType {
Other = 'OTHER',
}
export enum ChecksumAlgorithm {
sha1File = 'sha1-file', // sha1 checksum of the whole file contents
sha1Path = 'sha1-path', // sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated
}
export enum AssetFileType {
/**
* An full/large-size image extracted/converted from RAW photos

View File

@@ -250,6 +250,7 @@ where
select
"asset"."id",
"asset"."checksum",
"asset"."checksumAlgorithm",
"asset"."deviceAssetId",
"asset"."deviceId",
"asset"."fileCreatedAt",

View File

@@ -123,13 +123,13 @@ with
) as "year"
)
select
"a".*
"a".*,
to_json("asset_exif") as "exifInfo"
from
"today"
inner join lateral (
select
"asset"."id",
"asset"."localDateTime"
"asset".*
from
"asset"
inner join "asset_job_status" on "asset"."id" = "asset_job_status"."assetId"
@@ -151,6 +151,7 @@ with
limit
$7
) as "a" on true
inner join "asset_exif" on "a"."id" = "asset_exif"."assetId"
)
select
date_part(

View File

@@ -404,7 +404,7 @@ export class AssetRepository {
(qb) =>
qb
.selectFrom('asset')
.select(['asset.id', 'asset.localDateTime'])
.selectAll('asset')
.innerJoin('asset_job_status', 'asset.id', 'asset_job_status.assetId')
.where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
.where('asset.ownerId', '=', anyUuid(ownerIds))
@@ -423,7 +423,9 @@ export class AssetRepository {
.as('a'),
(join) => join.onTrue(),
)
.selectAll('a'),
.innerJoin('asset_exif', 'a.id', 'asset_exif.assetId')
.selectAll('a')
.select((eb) => eb.fn.toJson(eb.table('asset_exif')).as('exifInfo')),
)
.selectFrom('res')
.select(sql<number>`date_part('year', ("localDateTime" at time zone 'UTC')::date)::int`.as('year'))

View File

@@ -1,5 +1,5 @@
import { registerEnum } from '@immich/sql-tools';
import { AssetStatus, AssetVisibility, SourceType } from 'src/enum';
import { AssetStatus, AssetVisibility, ChecksumAlgorithm, SourceType } from 'src/enum';
export const assets_status_enum = registerEnum({
name: 'assets_status_enum',
@@ -15,3 +15,8 @@ export const asset_visibility_enum = registerEnum({
name: 'asset_visibility_enum',
values: Object.values(AssetVisibility),
});
export const asset_checksum_algorithm_enum = registerEnum({
name: 'asset_checksum_algorithm_enum',
values: Object.values(ChecksumAlgorithm),
});

View File

@@ -0,0 +1,35 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE TYPE "asset_checksum_algorithm_enum" AS ENUM ('sha1-file','sha1-path');`.execute(db);
await sql`ALTER TABLE "asset" ADD "checksumAlgorithm" asset_checksum_algorithm_enum;`.execute(db);
// Update in batches to handle millions of rows efficiently
const batchSize = 10_000;
let updatedRows: number;
do {
const result = await sql`
UPDATE "asset"
SET "checksumAlgorithm" = CASE
WHEN "isExternal" = true THEN 'sha1-path'::asset_checksum_algorithm_enum
ELSE 'sha1-file'::asset_checksum_algorithm_enum
END
WHERE "id" IN (
SELECT "id"
FROM "asset"
WHERE "checksumAlgorithm" IS NULL
LIMIT ${batchSize}
)
`.execute(db);
updatedRows = Number(result.numAffectedRows ?? 0);
} while (updatedRows > 0);
await sql`ALTER TABLE "asset" ALTER COLUMN "checksumAlgorithm" SET NOT NULL;`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "asset" DROP COLUMN "checksumAlgorithm";`.execute(db);
await sql`DROP TYPE "asset_checksum_algorithm_enum";`.execute(db);
}

View File

@@ -12,8 +12,8 @@ import {
UpdateDateColumn,
} from '@immich/sql-tools';
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
import { asset_checksum_algorithm_enum, asset_visibility_enum, assets_status_enum } from 'src/schema/enums';
import { asset_delete_audit } from 'src/schema/functions';
import { LibraryTable } from 'src/schema/tables/library.table';
import { StackTable } from 'src/schema/tables/stack.table';
@@ -98,6 +98,9 @@ export class AssetTable {
@Column({ type: 'bytea', index: true })
checksum!: Buffer; // sha1 checksum
@Column({ enum: asset_checksum_algorithm_enum })
checksumAlgorithm!: ChecksumAlgorithm;
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
livePhotoVideoId!: string | null;

View File

@@ -27,6 +27,7 @@ import {
AssetStatus,
AssetVisibility,
CacheControl,
ChecksumAlgorithm,
JobName,
Permission,
StorageFolder,
@@ -409,6 +410,7 @@ export class AssetMediaService extends BaseService {
deviceId: asset.deviceId,
type: asset.type,
checksum: asset.checksum,
checksumAlgorithm: asset.checksumAlgorithm,
fileCreatedAt: asset.fileCreatedAt,
localDateTime: asset.localDateTime,
fileModifiedAt: asset.fileModifiedAt,
@@ -430,6 +432,7 @@ export class AssetMediaService extends BaseService {
libraryId: null,
checksum: file.checksum,
checksumAlgorithm: ChecksumAlgorithm.sha1File,
originalPath: file.originalPath,
deviceAssetId: dto.deviceAssetId,

View File

@@ -17,7 +17,17 @@ import {
ValidateLibraryImportPathResponseDto,
ValidateLibraryResponseDto,
} from 'src/dtos/library.dto';
import { AssetStatus, AssetType, CronJob, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
import {
AssetStatus,
AssetType,
ChecksumAlgorithm,
CronJob,
DatabaseLock,
ImmichWorker,
JobName,
JobStatus,
QueueName,
} from 'src/enum';
import { ArgOf } from 'src/repositories/event.repository';
import { AssetSyncResult } from 'src/repositories/library.repository';
import { AssetTable } from 'src/schema/tables/asset.table';
@@ -400,6 +410,7 @@ export class LibraryService extends BaseService {
ownerId,
libraryId,
checksum: this.cryptoRepository.hashSha1(`path:${assetPath}`),
checksumAlgorithm: ChecksumAlgorithm.sha1Path,
originalPath: assetPath,
fileCreatedAt: stat.mtime,

View File

@@ -7,6 +7,7 @@ import {
AssetFileType,
AssetType,
AssetVisibility,
ChecksumAlgorithm,
ExifOrientation,
ImmichWorker,
JobName,
@@ -651,6 +652,7 @@ describe(MetadataService.name, () => {
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
@@ -704,6 +706,7 @@ describe(MetadataService.name, () => {
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(asset.id);
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,
@@ -757,6 +760,7 @@ describe(MetadataService.name, () => {
expect(mocks.storage.readFile).toHaveBeenCalledWith(asset.originalPath, expect.any(Object));
expect(mocks.asset.create).toHaveBeenCalledWith({
checksum: expect.any(Buffer),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: 'NONE',
deviceId: 'NONE',
fileCreatedAt: asset.fileCreatedAt,

View File

@@ -14,6 +14,7 @@ import {
AssetFileType,
AssetType,
AssetVisibility,
ChecksumAlgorithm,
DatabaseLock,
ExifOrientation,
ImmichWorker,
@@ -675,6 +676,7 @@ export class MetadataService extends BaseService {
fileModifiedAt: stats.mtime,
localDateTime: dates.localDateTime,
checksum,
checksumAlgorithm: ChecksumAlgorithm.sha1File,
ownerId: asset.ownerId,
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
originalFileName: `${parse(asset.originalFileName).name}.mp4`,

View File

@@ -1,5 +1,5 @@
import { Selectable } from 'kysely';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum';
import { AssetTable } from 'src/schema/tables/asset.table';
import { StackTable } from 'src/schema/tables/stack.table';
import { AssetEditFactory } from 'test/factories/asset-edit.factory';
@@ -51,6 +51,7 @@ export class AssetFactory {
updateId: newUuidV7(),
status: AssetStatus.Active,
checksum: newSha1(),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: '',
deviceId: '',
duplicateId: null,

View File

@@ -1,7 +1,7 @@
import { UserAdmin } from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm, SharedLinkType } from 'src/enum';
import { AssetFactory } from 'test/factories/asset.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';
@@ -94,6 +94,7 @@ export const sharedLinkStub = {
type: AssetType.Video,
originalPath: 'fake_path/jpeg',
checksum: Buffer.from('file hash', 'utf8'),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
fileModifiedAt: today,
fileCreatedAt: today,
localDateTime: today,

View File

@@ -11,6 +11,7 @@ import {
AlbumUserRole,
AssetType,
AssetVisibility,
ChecksumAlgorithm,
MemoryType,
SourceType,
SyncEntityType,
@@ -535,6 +536,7 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
deviceId: '',
originalFileName: '',
checksum: randomBytes(32),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
type: AssetType.Image,
originalPath: '/path/to/something.jpg',
ownerId: 'not-a-valid-uuid',

View File

@@ -28,6 +28,7 @@ import {
AssetStatus,
AssetType,
AssetVisibility,
ChecksumAlgorithm,
MemoryType,
Permission,
SourceType,
@@ -249,6 +250,7 @@ const assetFactory = (
updateId: newUuidV7(),
status: AssetStatus.Active,
checksum: newSha1(),
checksumAlgorithm: ChecksumAlgorithm.sha1File,
deviceAssetId: '',
deviceId: '',
duplicateId: null,