mirror of
https://github.com/immich-app/immich.git
synced 2026-02-07 10:19:37 +03:00
Compare commits
1 Commits
feat/debug
...
fix/25803
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e69052478 |
@@ -2221,6 +2221,71 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/albums/{id}/thumbnail": {
|
||||
"get": {
|
||||
"description": "Virtual route that redirects to the thumbnail of the album cover asset.",
|
||||
"operationId": "getAlbumThumbnailRedirect",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Redirect to album thumbnail",
|
||||
"tags": [
|
||||
"Albums"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Beta"
|
||||
}
|
||||
],
|
||||
"x-immich-permission": "album.read",
|
||||
"x-immich-state": "Beta"
|
||||
}
|
||||
},
|
||||
"/albums/{id}/user/{userId}": {
|
||||
"delete": {
|
||||
"description": "Remove a user from an album. Use an ID of \"me\" to leave a shared album.",
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
@Command({
|
||||
name: 'debug-migrations',
|
||||
description: 'Run a report to debug issues with database migrations',
|
||||
})
|
||||
export class DebugMigrations extends CommandRunner {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const report = await this.service.debugMigrations();
|
||||
|
||||
const maxLength = Math.max(...report.results.map((item) => item.name.length));
|
||||
|
||||
const success = report.results.filter((item) => item.status === 'applied');
|
||||
const deleted = report.results.filter((item) => item.status === 'deleted');
|
||||
const missing = report.results.filter((item) => item.status === 'missing');
|
||||
|
||||
for (const item of report.results) {
|
||||
const name = item.name.padEnd(maxLength, ' ');
|
||||
switch (item.status) {
|
||||
case 'applied': {
|
||||
console.log(`✅ ${name}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'deleted': {
|
||||
console.log(`❌ ${name} - Deleted! (this migration does not exist anymore)`);
|
||||
break;
|
||||
}
|
||||
|
||||
case 'missing': {
|
||||
console.log(`⚠️ ${name} - Missing! (this migration needs to be applied still)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length === 0 && deleted.length === 0) {
|
||||
console.log(`\nAll ${success.length} migrations have been successfully applied! 🎉`);
|
||||
} else {
|
||||
console.log(`\nMigration issues detected:`);
|
||||
console.log(` Missing migrations: ${missing.length}`);
|
||||
console.log(` Deleted migrations: ${deleted.length}`);
|
||||
console.log(` Successfully applied migrations: ${success.length}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Unable to debug migrations');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Command({
|
||||
name: 'debug-schema',
|
||||
description: 'Run a report to debug issues with database schema',
|
||||
})
|
||||
export class DebugSchema extends CommandRunner {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const output = await this.service.debugSchema();
|
||||
|
||||
if (output.length === 0) {
|
||||
console.log('No schema changes detected');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(output.join('\n'));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Unable to debug schema');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { DebugMigrations, DebugSchema } from 'src/commands/debug-migrations.command';
|
||||
import { GrantAdminCommand, PromptEmailQuestion, RevokeAdminCommand } from 'src/commands/grant-admin';
|
||||
import { ListUsersCommand } from 'src/commands/list-users.command';
|
||||
import { DisableMaintenanceModeCommand, EnableMaintenanceModeCommand } from 'src/commands/maintenance-mode';
|
||||
@@ -29,6 +28,4 @@ export const commandsAndQuestions = [
|
||||
ChangeMediaLocationCommand,
|
||||
PromptMediaLocationQuestions,
|
||||
PromptConfirmMoveQuestions,
|
||||
DebugMigrations,
|
||||
DebugSchema,
|
||||
];
|
||||
|
||||
@@ -1,4 +1,17 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Put, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Query,
|
||||
Redirect,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { Endpoint, HistoryBuilder } from 'src/decorators';
|
||||
import {
|
||||
@@ -73,6 +86,19 @@ export class AlbumController {
|
||||
return this.service.get(auth, id, dto);
|
||||
}
|
||||
|
||||
@Authenticated({ permission: Permission.AlbumRead, sharedLink: true })
|
||||
@Get(':id/thumbnail')
|
||||
@Redirect()
|
||||
@Endpoint({
|
||||
summary: 'Redirect to album thumbnail',
|
||||
description: 'Virtual route that redirects to the thumbnail of the album cover asset.',
|
||||
history: new HistoryBuilder().added('v2.6.0').beta('v2.6.0'),
|
||||
})
|
||||
async getAlbumThumbnailRedirect(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
|
||||
const url = await this.service.getThumbnailRedirectUrl(auth, id);
|
||||
return { url, status: 307 };
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Authenticated({ permission: Permission.AlbumUpdate })
|
||||
@Endpoint({
|
||||
|
||||
@@ -87,6 +87,16 @@ export class AlbumRepository {
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getForThumbnailRedirect(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.innerJoin('album', 'album.albumThumbnailAssetId', 'asset.id')
|
||||
.where('album.id', '=', id)
|
||||
.select(['asset.id', 'asset.thumbhash'])
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async getByAssetId(ownerId: string, assetId: string) {
|
||||
return this.db
|
||||
|
||||
@@ -281,14 +281,6 @@ export class DatabaseRepository {
|
||||
return rows[0].db;
|
||||
}
|
||||
|
||||
async getMigrations() {
|
||||
const { rows } = await sql<{
|
||||
name: string;
|
||||
timestamp: string;
|
||||
}>`SELECT * FROM kysely_migrations ORDER BY name ASC`.execute(this.db);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async getDimensionSize(table: string, column = 'embedding'): Promise<number> {
|
||||
const { rows } = await sql<{ dimsize: number }>`
|
||||
SELECT atttypmod as dimsize
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Permission } from 'src/enum';
|
||||
import { AlbumAssetCount, AlbumInfoOptions } from 'src/repositories/album.repository';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
|
||||
@Injectable()
|
||||
@@ -93,6 +94,23 @@ export class AlbumService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
async getThumbnailRedirectUrl(auth: AuthDto, id: string) {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
|
||||
|
||||
const asset = await this.albumRepository.getForThumbnailRedirect(id);
|
||||
if (!asset) {
|
||||
throw new BadRequestException('Album has no thumbnail');
|
||||
}
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('edited', 'true');
|
||||
if (asset.thumbhash) {
|
||||
params.append('c', hexOrBufferToBase64(asset.thumbhash));
|
||||
}
|
||||
|
||||
return `/api/assets/${asset.id}/thumbnail?${params.toString()}`;
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: CreateAlbumDto): Promise<AlbumResponseDto> {
|
||||
const albumUsers = dto.albumUsers || [];
|
||||
|
||||
|
||||
@@ -1,80 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isAbsolute, join } from 'node:path';
|
||||
import postgres from 'postgres';
|
||||
import { isAbsolute } from 'node:path';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||
import { MaintenanceAction, SystemMetadataKey } from 'src/enum';
|
||||
import { BaseService } from 'src/services/base.service';
|
||||
import { schemaDiff, schemaFromCode, schemaFromDatabase } from 'src/sql-tools';
|
||||
import { asPostgresConnectionConfig } from 'src/utils/database';
|
||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance';
|
||||
import { getExternalDomain } from 'src/utils/misc';
|
||||
|
||||
export type MigrationReport = {
|
||||
files: string[];
|
||||
rows: Array<{ name: string; timestamp: string }>;
|
||||
results: MigrationStatus[];
|
||||
};
|
||||
type MigrationStatus = {
|
||||
name: string;
|
||||
status: 'applied' | 'missing' | 'deleted';
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class CliService extends BaseService {
|
||||
async debugMigrations(): Promise<MigrationReport> {
|
||||
// eslint-disable-next-line unicorn/prefer-module
|
||||
const allFiles = await this.storageRepository.readdir(join(__dirname, '../schema/migrations'));
|
||||
const files = allFiles.filter((file) => file.endsWith('.js')).map((file) => file.slice(0, -3));
|
||||
const rows = await this.databaseRepository.getMigrations();
|
||||
const filesSet = new Set(files);
|
||||
const rowsSet = new Set(rows.map((item) => item.name));
|
||||
const combined = [...filesSet, ...rowsSet].toSorted();
|
||||
|
||||
const results: MigrationStatus[] = [];
|
||||
|
||||
for (const name of combined) {
|
||||
if (filesSet.has(name) && rowsSet.has(name)) {
|
||||
results.push({ name, status: 'applied' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (filesSet.has(name) && !rowsSet.has(name)) {
|
||||
results.push({ name, status: 'missing' });
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!filesSet.has(name) && rowsSet.has(name)) {
|
||||
results.push({ name, status: 'deleted' });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return { files, rows, results };
|
||||
}
|
||||
|
||||
async debugSchema() {
|
||||
const source = schemaFromCode({ overrides: true, namingStrategy: 'default' });
|
||||
const { database } = this.configRepository.getEnv();
|
||||
const db = postgres(asPostgresConnectionConfig(database.config));
|
||||
const target = await schemaFromDatabase(db, {});
|
||||
|
||||
console.log(source.warnings.join('\n'));
|
||||
|
||||
const up = schemaDiff(source, target, {
|
||||
tables: { ignoreExtra: true },
|
||||
functions: { ignoreExtra: false },
|
||||
parameters: { ignoreExtra: true },
|
||||
});
|
||||
|
||||
if (up.items.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return up.asSql();
|
||||
}
|
||||
|
||||
async listUsers(): Promise<UserAdminResponseDto[]> {
|
||||
const users = await this.userRepository.getList({ withDeleted: true });
|
||||
return users.map((user) => mapUserAdmin(user));
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
|
||||
import NoCover from '$lib/components/sharedlinks-page/covers/no-cover.svelte';
|
||||
import { getAssetMediaUrl } from '$lib/utils';
|
||||
import { type AlbumResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -14,9 +13,7 @@
|
||||
let { album, preload = false, class: className = '' }: Props = $props();
|
||||
|
||||
let alt = $derived(album.albumName || $t('unnamed_album'));
|
||||
let thumbnailUrl = $derived(
|
||||
album.albumThumbnailAssetId ? getAssetMediaUrl({ id: album.albumThumbnailAssetId }) : null,
|
||||
);
|
||||
let thumbnailUrl = $derived(album.albumThumbnailAssetId ? `/api/albums/${album.id}/thumbnail` : null);
|
||||
</script>
|
||||
|
||||
{#if thumbnailUrl}
|
||||
|
||||
Reference in New Issue
Block a user