Compare commits

..

5 Commits

Author SHA1 Message Date
Alex f3af54c6c9 chore: more logs and feedback 2025-08-29 16:18:52 -05:00
Sergey Katsubo f5954f4c9b chore(docs): Avoid /data in external library examples (#21357)
* Avoid /data for external libraries

* Remove mention of microservice containers

* Update docs/docs/features/libraries.md

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>

---------

Co-authored-by: Matthew Momjian <50788000+mmomjian@users.noreply.github.com>
2025-08-29 10:24:21 -05:00
Min Idzelis 147accd957 fix: fix docker perms for dev (#21359) 2025-08-28 22:07:29 -04:00
Mert 9487241481 fix(server): refresh faces query (#21380) 2025-08-28 20:23:40 -04:00
Sergey Katsubo 460e1d4715 fix(server): folder sort order (#21383) 2025-08-28 20:22:40 -04:00
18 changed files with 152 additions and 44 deletions
@@ -26,7 +26,7 @@ services:
env_file: !reset [] env_file: !reset []
init: init:
env_file: !reset [] env_file: !reset []
command: sh -c 'for path in /data /data/upload /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' command: sh -c 'find /data -maxdepth 1 ! -path "/data/postgres" -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
immich-machine-learning: immich-machine-learning:
env_file: !reset [] env_file: !reset []
database: database:
+2 -1
View File
@@ -569,7 +569,8 @@ jobs:
- name: Build the app - name: Build the app
run: pnpm --filter immich build run: pnpm --filter immich build
- name: Run API generation - name: Run API generation
run: make open-api run: ./bin/generate-open-api.sh
working-directory: open-api
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4 uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files id: verify-changed-files
+28 -10
View File
@@ -60,20 +60,37 @@ VOLUME_DIRS = \
./e2e/node_modules \ ./e2e/node_modules \
./docs/node_modules \ ./docs/node_modules \
./server/node_modules \ ./server/node_modules \
./server/dist \
./open-api/typescript-sdk/node_modules \ ./open-api/typescript-sdk/node_modules \
./.github/node_modules \ ./.github/node_modules \
./node_modules \ ./node_modules \
./cli/node_modules ./cli/node_modules
# create empty directories and chown to current user # Include .env file if it exists
-include docker/.env
# Helper function to chown, on error suggest remediation and exit
define safe_chown
if chown $(2) $(or $(UID),1000):$(or $(GID),1000) "$(1)" 2>/dev/null; then \
true; \
else \
echo "Permission denied when changing owner of volumes and upload location. Try running 'sudo make prepare-volumes' first."; \
exit 1; \
fi;
endef
# create empty directories and chown
prepare-volumes: prepare-volumes:
@for dir in $(VOLUME_DIRS); do \ @$(foreach dir,$(VOLUME_DIRS),mkdir -p $(dir);)
mkdir -p $$dir; \ @$(foreach dir,$(VOLUME_DIRS),$(call safe_chown,$(dir),-R))
done ifneq ($(UPLOAD_LOCATION),)
@if [ -n "$(VOLUME_DIRS)" ]; then \ ifeq ($(filter /%,$(UPLOAD_LOCATION)),)
chown -R $$(id -u):$$(id -g) $(VOLUME_DIRS); \ @mkdir -p "docker/$(UPLOAD_LOCATION)"
fi @$(call safe_chown,docker/$(UPLOAD_LOCATION),)
else
@mkdir -p "$(UPLOAD_LOCATION)"
@$(call safe_chown,$(UPLOAD_LOCATION),)
endif
endif
MODULES = e2e server web cli sdk docs .github MODULES = e2e server web cli sdk docs .github
@@ -150,8 +167,9 @@ clean:
find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' + find . -name ".svelte-kit" -type d -prune -exec rm -rf '{}' +
find . -name "coverage" -type d -prune -exec rm -rf '{}' + find . -name "coverage" -type d -prune -exec rm -rf '{}' +
find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' + find . -name ".pnpm-store" -type d -prune -exec rm -rf '{}' +
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml down -v --remove-orphans || true
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml down -v --remove-orphans || true
setup-server-dev: install-server setup-server-dev: install-server
setup-web-dev: install-sdk build-sdk install-web setup-web-dev: install-sdk build-sdk install-web
+1 -1
View File
@@ -189,7 +189,7 @@ services:
env_file: env_file:
- .env - .env
user: 0:0 user: 0:0
command: sh -c 'for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done' command: sh -c 'find /data -maxdepth 1 -type d -exec chown ${UID:-1000}:${GID:-1000} {} + 2>/dev/null || true; for path in /usr/src/app/.pnpm-store /usr/src/app/server/node_modules /usr/src/app/server/dist /usr/src/app/.github/node_modules /usr/src/app/cli/node_modules /usr/src/app/docs/node_modules /usr/src/app/e2e/node_modules /usr/src/app/open-api/typescript-sdk/node_modules /usr/src/app/web/.svelte-kit /usr/src/app/web/coverage /usr/src/app/node_modules /usr/src/app/web/node_modules; do [ -e "$$path" ] && chown -R ${UID:-1000}:${GID:-1000} "$$path" || true; done'
volumes: volumes:
- pnpm-store:/usr/src/app/.pnpm-store - pnpm-store:/usr/src/app/.pnpm-store
- server-node_modules:/usr/src/app/server/node_modules - server-node_modules:/usr/src/app/server/node_modules
+1 -1
View File
@@ -33,7 +33,7 @@ Sometimes, an external library will not scan correctly. This can happen if Immic
- Are the permissions set correctly? - Are the permissions set correctly?
- Make sure you are using forward slashes (`/`) and not backward slashes. - Make sure you are using forward slashes (`/`) and not backward slashes.
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the same in any microservices containers. To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/mnt/photos`, check it with `ls /mnt/photos`. If you are using a dedicated microservices container, make sure to add the same mount point and check for availability within the microservices container as well.
### Exclusion Patterns ### Exclusion Patterns
+2
View File
@@ -639,6 +639,8 @@
"cannot_update_the_description": "Cannot update the description", "cannot_update_the_description": "Cannot update the description",
"cast": "Cast", "cast": "Cast",
"cast_description": "Configure available cast destinations", "cast_description": "Configure available cast destinations",
"cellular_data_for_photos": "Cellular data for photos",
"cellular_data_for_videos": "Cellular data for videos",
"change_date": "Change date", "change_date": "Change date",
"change_description": "Change description", "change_description": "Change description",
"change_display_order": "Change display order", "change_display_order": "Change display order",
+1 -1
View File
@@ -65,7 +65,7 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
pnpm install --frozen-lockfile --prefix server pnpm install --frozen-lockfile --prefix server
pnpm --prefix server run build pnpm --prefix server run build
make open-api ( cd ./open-api && bash ./bin/generate-open-api.sh )
jq --arg version "$NEXT_SERVER" '.version = $version' open-api/typescript-sdk/package.json > open-api/typescript-sdk/package.json.tmp && mv open-api/typescript-sdk/package.json.tmp open-api/typescript-sdk/package.json jq --arg version "$NEXT_SERVER" '.version = $version' open-api/typescript-sdk/package.json > open-api/typescript-sdk/package.json.tmp && mv open-api/typescript-sdk/package.json.tmp open-api/typescript-sdk/package.json
+3 -9
View File
@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 77; objectVersion = 54;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -129,6 +129,8 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync; path = Sync;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -505,14 +507,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -541,14 +539,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+2 -2
View File
@@ -75,8 +75,8 @@ enum StoreKey<T> {
betaPromptShown<bool>._(1001), betaPromptShown<bool>._(1001),
betaTimeline<bool>._(1002), betaTimeline<bool>._(1002),
enableBackup<bool>._(1003), enableBackup<bool>._(1003),
useWifiForUploadVideos<bool>._(1004), useCellularForUploadVideos<bool>._(1004),
useWifiForUploadPhotos<bool>._(1005); useCellularForUploadPhotos<bool>._(1005);
const StoreKey._(this.id); const StoreKey._(this.id);
final int id; final int id;
@@ -3,6 +3,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; 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/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
@@ -93,7 +95,11 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
const _BackupCard(), const _BackupCard(),
const _RemainderCard(), const _RemainderCard(),
const Divider(), const Divider(),
const SizedBox(height: 4),
const _CellularBackupStatus(),
const SizedBox(height: 4),
BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()), BackupToggleButton(onStart: () async => await startBackup(), onStop: () async => await stopBackup()),
TextButton.icon( TextButton.icon(
icon: const Icon(Icons.info_outline_rounded), icon: const Icon(Icons.info_outline_rounded),
onPressed: () => context.pushRoute(const DriftUploadDetailRoute()), onPressed: () => context.pushRoute(const DriftUploadDetailRoute()),
@@ -109,6 +115,64 @@ class _DriftBackupPageState extends ConsumerState<DriftBackupPage> {
} }
} }
class _CellularBackupStatus extends ConsumerWidget {
const _CellularBackupStatus();
@override
Widget build(BuildContext context, WidgetRef ref) {
final cellularReqForVideos = Store.watch(StoreKey.useCellularForUploadVideos);
final cellularReqForPhotos = Store.watch(StoreKey.useCellularForUploadPhotos);
return GestureDetector(
onTap: () => context.pushRoute(const DriftBackupOptionsRoute()),
child: Row(
children: [
StreamBuilder(
stream: cellularReqForVideos,
initialData: Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false,
builder: (context, snapshot) {
return Expanded(
child: ListTile(
visualDensity: VisualDensity.compact,
leading: Icon(
snapshot.data ?? false ? Icons.check_circle : Icons.cancel_outlined,
size: 16,
color: snapshot.data ?? false ? Colors.green : context.colorScheme.onSurfaceVariant,
),
title: Text(
"cellular_data_for_videos".t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
),
);
},
),
StreamBuilder(
stream: cellularReqForPhotos,
initialData: Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false,
builder: (context, snapshot) {
return Expanded(
child: ListTile(
visualDensity: VisualDensity.compact,
leading: Icon(
snapshot.data ?? false ? Icons.check_circle : Icons.cancel_outlined,
size: 16,
color: snapshot.data ?? false ? Colors.green : context.colorScheme.onSurfaceVariant,
),
title: Text(
"cellular_data_for_photos".t(context: context),
style: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.onSurfaceVariant),
),
),
);
},
),
],
),
);
}
}
class _BackupAlbumSelectionCard extends ConsumerWidget { class _BackupAlbumSelectionCard extends ConsumerWidget {
const _BackupAlbumSelectionCard(); const _BackupAlbumSelectionCard();
@@ -17,15 +17,15 @@ class DriftBackupOptionsPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
bool hasPopped = false; bool hasPopped = false;
final previousWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; final previousWifiReqForVideos = Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false;
final previousWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; final previousWifiReqForPhotos = Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false;
return PopScope( return PopScope(
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
// There is an issue with Flutter where the pop event // There is an issue with Flutter where the pop event
// can be triggered multiple times, so we guard it with _hasPopped // can be triggered multiple times, so we guard it with _hasPopped
final currentWifiReqForVideos = Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false; final currentWifiReqForVideos = Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false;
final currentWifiReqForPhotos = Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false; final currentWifiReqForPhotos = Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false;
if (currentWifiReqForVideos == previousWifiReqForVideos && if (currentWifiReqForVideos == previousWifiReqForVideos &&
currentWifiReqForPhotos == previousWifiReqForPhotos) { currentWifiReqForPhotos == previousWifiReqForPhotos) {
@@ -48,8 +48,8 @@ enum AppSettingsEnum<T> {
photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true), photoManagerCustomFilter<bool>(StoreKey.photoManagerCustomFilter, null, true),
betaTimeline<bool>(StoreKey.betaTimeline, null, false), betaTimeline<bool>(StoreKey.betaTimeline, null, false),
enableBackup<bool>(StoreKey.enableBackup, null, false), enableBackup<bool>(StoreKey.enableBackup, null, false),
useCellularForUploadVideos<bool>(StoreKey.useWifiForUploadVideos, null, false), useCellularForUploadVideos<bool>(StoreKey.useCellularForUploadVideos, null, false),
useCellularForUploadPhotos<bool>(StoreKey.useWifiForUploadPhotos, null, false), useCellularForUploadPhotos<bool>(StoreKey.useCellularForUploadPhotos, null, false),
readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false); readonlyModeEnabled<bool>(StoreKey.readonlyModeEnabled, "readonlyModeEnabled", false);
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
+31 -4
View File
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
final uploadServiceProvider = Provider((ref) { final uploadServiceProvider = Provider((ref) {
@@ -57,6 +58,7 @@ class UploadService {
Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream; Stream<TaskStatusUpdate> get taskStatusStream => _taskStatusController.stream;
Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream; Stream<TaskProgressUpdate> get taskProgressStream => _taskProgressController.stream;
final Logger _log = Logger('UploadService');
bool shouldAbortQueuingTasks = false; bool shouldAbortQueuingTasks = false;
@@ -127,11 +129,16 @@ class UploadService {
final candidates = await _backupRepository.getCandidates(userId); final candidates = await _backupRepository.getCandidates(userId);
if (candidates.isEmpty) { if (candidates.isEmpty) {
_log.info("No backup candidates found for user $userId");
return; return;
} }
_log.info("Starting backup for ${candidates.length} candidates");
onEnqueueTasks(EnqueueStatus(enqueueCount: 0, totalCount: candidates.length));
const batchSize = 100; const batchSize = 100;
int count = 0; int count = 0;
int skippedAssets = 0;
for (int i = 0; i < candidates.length; i += batchSize) { for (int i = 0; i < candidates.length; i += batchSize) {
if (shouldAbortQueuingTasks) { if (shouldAbortQueuingTasks) {
break; break;
@@ -144,16 +151,22 @@ class UploadService {
final task = await _getUploadTask(asset); final task = await _getUploadTask(asset);
if (task != null) { if (task != null) {
tasks.add(task); tasks.add(task);
} else {
skippedAssets++;
_log.warning("Skipped asset ${asset.id} (${asset.name}) - unable to create upload task");
} }
} }
if (tasks.isNotEmpty && !shouldAbortQueuingTasks) { count += tasks.length;
count += tasks.length; onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length));
await enqueueTasks(tasks);
onEnqueueTasks(EnqueueStatus(enqueueCount: count, totalCount: candidates.length)); if (tasks.isNotEmpty && !shouldAbortQueuingTasks) {
_log.info("Enqueuing ${tasks.length} upload tasks");
await enqueueTasks(tasks);
} }
} }
_log.info("Upload queueing completed: $count tasks enqueued, $skippedAssets assets skipped");
} }
// Enqueue All does not work from the background on Android yet. This method is a temporary workaround // Enqueue All does not work from the background on Android yet. This method is a temporary workaround
@@ -165,9 +178,14 @@ class UploadService {
final candidates = await _backupRepository.getCandidates(userId); final candidates = await _backupRepository.getCandidates(userId);
if (candidates.isEmpty) { if (candidates.isEmpty) {
debugPrint("No backup candidates found for serial backup");
return; return;
} }
debugPrint("Starting serial backup for ${candidates.length} candidates");
int skippedAssets = 0;
int enqueuedTasks = 0;
for (final asset in candidates) { for (final asset in candidates) {
if (shouldAbortQueuingTasks) { if (shouldAbortQueuingTasks) {
break; break;
@@ -176,8 +194,13 @@ class UploadService {
final task = await _getUploadTask(asset); final task = await _getUploadTask(asset);
if (task != null) { if (task != null) {
await _uploadRepository.enqueueBackground(task); await _uploadRepository.enqueueBackground(task);
enqueuedTasks++;
} else {
skippedAssets++;
} }
} }
debugPrint("Serial backup completed: $enqueuedTasks tasks enqueued, $skippedAssets assets skipped");
} }
/// Cancel all ongoing uploads and reset the upload queue /// Cancel all ongoing uploads and reset the upload queue
@@ -245,6 +268,7 @@ class UploadService {
Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async { Future<UploadTask?> _getUploadTask(LocalAsset asset, {String group = kBackupGroup, int? priority}) async {
final entity = await _storageRepository.getAssetEntityForAsset(asset); final entity = await _storageRepository.getAssetEntityForAsset(asset);
if (entity == null) { if (entity == null) {
_log.warning("Cannot get AssetEntity for asset ${asset.id} (${asset.name}) created on ${asset.createdAt}");
return null; return null;
} }
@@ -267,6 +291,9 @@ class UploadService {
} }
if (file == null) { if (file == null) {
_log.warning(
"Cannot get file for asset ${asset.id} (${asset.name}) created on ${asset.createdAt} - file may have been deleted or moved",
);
return null; return null;
} }
@@ -22,7 +22,7 @@ class _UseWifiForUploadVideosButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final valueStream = Store.watch(StoreKey.useWifiForUploadVideos); final valueStream = Store.watch(StoreKey.useCellularForUploadVideos);
return ListTile( return ListTile(
title: Text( title: Text(
@@ -32,7 +32,7 @@ class _UseWifiForUploadVideosButton extends ConsumerWidget {
subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge), subtitle: Text("network_requirement_videos_upload".t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder( trailing: StreamBuilder(
stream: valueStream, stream: valueStream,
initialData: Store.tryGet(StoreKey.useWifiForUploadVideos) ?? false, initialData: Store.tryGet(StoreKey.useCellularForUploadVideos) ?? false,
builder: (context, snapshot) { builder: (context, snapshot) {
final value = snapshot.data ?? false; final value = snapshot.data ?? false;
return Switch( return Switch(
@@ -54,7 +54,7 @@ class _UseWifiForUploadPhotosButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final valueStream = Store.watch(StoreKey.useWifiForUploadPhotos); final valueStream = Store.watch(StoreKey.useCellularForUploadPhotos);
return ListTile( return ListTile(
title: Text( title: Text(
@@ -64,7 +64,7 @@ class _UseWifiForUploadPhotosButton extends ConsumerWidget {
subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge), subtitle: Text("network_requirement_photos_upload".t(context: context), style: context.textTheme.labelLarge),
trailing: StreamBuilder( trailing: StreamBuilder(
stream: valueStream, stream: valueStream,
initialData: Store.tryGet(StoreKey.useWifiForUploadPhotos) ?? false, initialData: Store.tryGet(StoreKey.useCellularForUploadPhotos) ?? false,
builder: (context, snapshot) { builder: (context, snapshot) {
final value = snapshot.data ?? false; final value = snapshot.data ?? false;
return Switch( return Switch(
+1 -2
View File
@@ -468,9 +468,8 @@ where
"asset"."visibility" != $1 "asset"."visibility" != $1
and "asset"."deletedAt" is null and "asset"."deletedAt" is null
and "job_status"."previewAt" is not null and "job_status"."previewAt" is not null
and "job_status"."facesRecognizedAt" is null
order by order by
"asset"."createdAt" desc "asset"."fileCreatedAt" desc
-- AssetJobRepository.streamForMigrationJob -- AssetJobRepository.streamForMigrationJob
select select
+2
View File
@@ -12,6 +12,8 @@ where
and "fileCreatedAt" is not null and "fileCreatedAt" is not null
and "fileModifiedAt" is not null and "fileModifiedAt" is not null
and "localDateTime" is not null and "localDateTime" is not null
order by
"directoryPath" asc
-- ViewRepository.getAssetsByOriginalPath -- ViewRepository.getAssetsByOriginalPath
select select
@@ -334,9 +334,9 @@ export class AssetJobRepository {
@GenerateSql({ params: [], stream: true }) @GenerateSql({ params: [], stream: true })
streamForDetectFacesJob(force?: boolean) { streamForDetectFacesJob(force?: boolean) {
return this.assetsWithPreviews() return this.assetsWithPreviews()
.$if(!force, (qb) => qb.where('job_status.facesRecognizedAt', 'is', null)) .$if(force === false, (qb) => qb.where('job_status.facesRecognizedAt', 'is', null))
.select(['asset.id']) .select(['asset.id'])
.orderBy('asset.createdAt', 'desc') .orderBy('asset.fileCreatedAt', 'desc')
.stream(); .stream();
} }
@@ -20,6 +20,7 @@ export class ViewRepository {
.where('fileCreatedAt', 'is not', null) .where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', 'is not', null) .where('fileModifiedAt', 'is not', null)
.where('localDateTime', 'is not', null) .where('localDateTime', 'is not', null)
.orderBy('directoryPath', 'asc')
.execute(); .execute();
return results.map((row) => row.directoryPath.replaceAll(/\/$/g, '')); return results.map((row) => row.directoryPath.replaceAll(/\/$/g, ''));