+
+## Set Storage Label For User
+
+The admin can add a custom label for each user, so instead of `upload/{userId}/your-template` it will be `upload/{custom_user_label}/your-template`.
+To apply a storage template, go to the Administration page -> click on the pencil button next to the user.
+:::note
+To apply the Storage Label to previously uploaded assets, run the Storage Migration Job.
+:::
+
+
## Password Reset
-To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to "password" and they have to change it next time the sign in.
+To reset a user's password, click the pencil icon to edit a user, then click "Reset Password". The user's password will be reset to random password and they have to change it next time the sign in.
-
+
+
+## Delete a User
+
+If you need to remove a user from Immich, head to "Administration", where users can be scheduled for deletion. The user account will immediately become disabled and their library and all associated data will be removed after 7 days by default.
+
+
+
+### Delete Delay
+
+You can customize the time of the deletion of the users from the Administration -> Settings -> User Settings.
+:::info user deletion job
+The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.
+:::
+
+
+
+### Immediately Remove User
+
+You can choose to delete a user immediately by checking the box
+`Queue user and assets for immediate deletion` in the deletion process, this will immediately remove the user and all assets.
+This cannot be undone and the files cannot be recovered.
+
+
diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts
index d8cea95b23..b9730aeef2 100644
--- a/e2e/src/api/specs/library.e2e-spec.ts
+++ b/e2e/src/api/specs/library.e2e-spec.ts
@@ -27,7 +27,7 @@ describe('/library', () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
user = await utils.userSetup(admin.accessToken, userDto.user1);
- library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
+ library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
websocket = await utils.connectWebsocket(admin.accessToken);
});
@@ -82,7 +82,7 @@ describe('/library', () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${user.accessToken}`)
- .send({ type: LibraryType.External });
+ .send({ ownerId: admin.userId, type: LibraryType.External });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
@@ -92,7 +92,7 @@ describe('/library', () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
- .send({ type: LibraryType.External });
+ .send({ ownerId: admin.userId, type: LibraryType.External });
expect(status).toBe(201);
expect(body).toEqual(
@@ -113,6 +113,7 @@ describe('/library', () => {
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
+ ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
@@ -133,6 +134,7 @@ describe('/library', () => {
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
+ ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path', '/path'],
@@ -148,6 +150,7 @@ describe('/library', () => {
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
+ ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
@@ -162,7 +165,7 @@ describe('/library', () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
- .send({ type: LibraryType.Upload });
+ .send({ ownerId: admin.userId, type: LibraryType.Upload });
expect(status).toBe(201);
expect(body).toEqual(
@@ -182,7 +185,7 @@ describe('/library', () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
- .send({ type: LibraryType.Upload, name: 'My Awesome Library' });
+ .send({ ownerId: admin.userId, type: LibraryType.Upload, name: 'My Awesome Library' });
expect(status).toBe(201);
expect(body).toEqual(
@@ -196,7 +199,7 @@ describe('/library', () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
- .send({ type: LibraryType.Upload, importPaths: ['/path/to/import'] });
+ .send({ ownerId: admin.userId, type: LibraryType.Upload, importPaths: ['/path/to/import'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
@@ -206,7 +209,7 @@ describe('/library', () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
- .send({ type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
+ .send({ ownerId: admin.userId, type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
@@ -330,7 +333,10 @@ describe('/library', () => {
});
it('should get library by id', async () => {
- const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
+ const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
+ type: LibraryType.External,
+ });
const { status, body } = await request(app)
.get(`/library/${library.id}`)
@@ -386,7 +392,10 @@ describe('/library', () => {
});
it('should delete an external library', async () => {
- const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
+ const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
+ type: LibraryType.External,
+ });
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
@@ -407,6 +416,7 @@ describe('/library', () => {
it('should delete an external library with assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
@@ -455,6 +465,7 @@ describe('/library', () => {
it('should not scan an upload library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
type: LibraryType.Upload,
});
@@ -468,6 +479,7 @@ describe('/library', () => {
it('should scan external library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
});
@@ -483,6 +495,7 @@ describe('/library', () => {
it('should scan external library with exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
exclusionPatterns: ['**/directoryA'],
@@ -499,6 +512,7 @@ describe('/library', () => {
it('should scan multiple import paths', async () => {
const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
});
@@ -515,6 +529,7 @@ describe('/library', () => {
it('should pick up new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`],
});
diff --git a/install.sh b/install.sh
index fc894ce21e..232ee1597e 100755
--- a/install.sh
+++ b/install.sh
@@ -6,7 +6,7 @@ ip_address=$(hostname -I | awk '{print $1}')
create_immich_directory() {
echo "Creating Immich directory..."
- mkdir -p ./immich-app/immich-data
+ mkdir -p ./immich-app
cd ./immich-app || exit
}
@@ -20,21 +20,6 @@ download_dot_env_file() {
curl -L https://github.com/immich-app/immich/releases/latest/download/example.env -o ./.env >/dev/null 2>&1
}
-replace_env_value() {
- KERNEL="$(uname -s | tr '[:upper:]' '[:lower:]')"
- if [ "$KERNEL" = "darwin" ]; then
- sed -i '' "s|$1=.*|$1=$2|" ./.env
- else
- sed -i "s|$1=.*|$1=$2|" ./.env
- fi
-}
-
-populate_upload_location() {
- echo "Populating default UPLOAD_LOCATION value..."
- upload_location=$(pwd)/immich-data
- replace_env_value "UPLOAD_LOCATION" "$upload_location"
-}
-
start_docker_compose() {
echo "Starting Immich's docker containers"
@@ -59,7 +44,6 @@ start_docker_compose() {
show_friendly_message() {
echo "Successfully deployed Immich!"
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
- echo "The library location is $upload_location"
echo "---------------------------------------------------"
echo "If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
@@ -75,5 +59,4 @@ show_friendly_message() {
create_immich_directory
download_docker_compose_file
download_dot_env_file
-populate_upload_location
start_docker_compose
diff --git a/mobile/openapi/doc/CreateLibraryDto.md b/mobile/openapi/doc/CreateLibraryDto.md
index e0caf1c8a5..94e96493ec 100644
--- a/mobile/openapi/doc/CreateLibraryDto.md
+++ b/mobile/openapi/doc/CreateLibraryDto.md
@@ -13,7 +13,7 @@ Name | Type | Description | Notes
**isVisible** | **bool** | | [optional]
**isWatched** | **bool** | | [optional]
**name** | **String** | | [optional]
-**ownerId** | **String** | | [optional]
+**ownerId** | **String** | |
**type** | [**LibraryType**](LibraryType.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart
index ef656ea2a3..24cc045300 100644
--- a/mobile/openapi/lib/model/create_library_dto.dart
+++ b/mobile/openapi/lib/model/create_library_dto.dart
@@ -18,7 +18,7 @@ class CreateLibraryDto {
this.isVisible,
this.isWatched,
this.name,
- this.ownerId,
+ required this.ownerId,
required this.type,
});
@@ -50,13 +50,7 @@ class CreateLibraryDto {
///
String? name;
- ///
- /// Please note: This property should have been non-nullable! Since the specification file
- /// does not include a default value (using the "default:" property), however, the generated
- /// source code must fall back to having a nullable type.
- /// Consider adding a "default:" property in the specification file to hide this note.
- ///
- String? ownerId;
+ String ownerId;
LibraryType type;
@@ -78,7 +72,7 @@ class CreateLibraryDto {
(isVisible == null ? 0 : isVisible!.hashCode) +
(isWatched == null ? 0 : isWatched!.hashCode) +
(name == null ? 0 : name!.hashCode) +
- (ownerId == null ? 0 : ownerId!.hashCode) +
+ (ownerId.hashCode) +
(type.hashCode);
@override
@@ -103,11 +97,7 @@ class CreateLibraryDto {
} else {
// json[r'name'] = null;
}
- if (this.ownerId != null) {
json[r'ownerId'] = this.ownerId;
- } else {
- // json[r'ownerId'] = null;
- }
json[r'type'] = this.type;
return json;
}
@@ -129,7 +119,7 @@ class CreateLibraryDto {
isVisible: mapValueOfTypeUNTRACKS FILES {extras.length > 0 ? `(${extras.length})` : ''}
+UNTRACKED FILES {extras.length > 0 ? `(${extras.length})` : ''}
These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug