feat(web,server): offline/untracked files admin tool (#4447)

* feat: admin repair orphans tool

* chore: open api

* fix: include upload folder

* fix: bugs

* feat: empty placeholder

* fix: checks

* feat: move buttons to top of page

* feat: styling and clipboard

* styling

* better clicking hitbox

* fix: show title on hover

* feat: download report

* restrict file access to immich related files

* Add description

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
Jason Rasmussen
2023-10-14 13:12:59 -04:00
committed by GitHub
parent ed386dd12a
commit d2807b8d6a
53 changed files with 3104 additions and 87 deletions

View File

@@ -1,6 +1,6 @@
import { AssetEntity, AssetPathType, PathType, PersonEntity, PersonPathType } from '@app/infra/entities';
import { Logger } from '@nestjs/common';
import { dirname, join } from 'node:path';
import { dirname, join, resolve } from 'node:path';
import { APP_MEDIA_LOCATION } from '../domain.constant';
import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
@@ -32,14 +32,14 @@ export class StorageCore {
) {}
getFolderLocation(folder: StorageFolder, userId: string) {
return join(this.getBaseFolder(folder), userId);
return join(StorageCore.getBaseFolder(folder), userId);
}
getLibraryFolder(user: { storageLabel: string | null; id: string }) {
return join(this.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
}
getBaseFolder(folder: StorageFolder) {
static getBaseFolder(folder: StorageFolder) {
return join(APP_MEDIA_LOCATION, folder);
}
@@ -64,7 +64,11 @@ export class StorageCore {
}
isAndroidMotionPath(originalPath: string) {
return originalPath.startsWith(this.getBaseFolder(StorageFolder.ENCODED_VIDEO));
return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO));
}
static isImmichPath(path: string) {
return resolve(path).startsWith(resolve(APP_MEDIA_LOCATION));
}
async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) {
@@ -135,7 +139,7 @@ export class StorageCore {
}
removeEmptyDirs(folder: StorageFolder) {
return this.repository.removeEmptyDirs(this.getBaseFolder(folder));
return this.repository.removeEmptyDirs(StorageCore.getBaseFolder(folder));
}
private savePath(pathType: PathType, id: string, newPath: string) {

View File

@@ -1,25 +1,14 @@
import {
newAssetRepositoryMock,
newMoveRepositoryMock,
newPersonRepositoryMock,
newStorageRepositoryMock,
} from '@test';
import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
import { newStorageRepositoryMock } from '@test';
import { IStorageRepository } from '../repositories';
import { StorageService } from './storage.service';
describe(StorageService.name, () => {
let sut: StorageService;
let assetMock: jest.Mocked<IAssetRepository>;
let moveMock: jest.Mocked<IMoveRepository>;
let personMock: jest.Mocked<IPersonRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
beforeEach(async () => {
assetMock = newAssetRepositoryMock();
moveMock = newMoveRepositoryMock();
personMock = newPersonRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new StorageService(assetMock, moveMock, personMock, storageMock);
sut = new StorageService(storageMock);
});
it('should work', () => {

View File

@@ -1,24 +1,16 @@
import { Inject, Injectable, Logger } from '@nestjs/common';
import { IDeleteFilesJob } from '../job';
import { IAssetRepository, IMoveRepository, IPersonRepository, IStorageRepository } from '../repositories';
import { IStorageRepository } from '../repositories';
import { StorageCore, StorageFolder } from './storage.core';
@Injectable()
export class StorageService {
private logger = new Logger(StorageService.name);
private storageCore: StorageCore;
constructor(
@Inject(IAssetRepository) assetRepository: IAssetRepository,
@Inject(IMoveRepository) private moveRepository: IMoveRepository,
@Inject(IPersonRepository) personRepository: IPersonRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
}
constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {}
init() {
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY);
this.storageRepository.mkdirSync(libraryBase);
}