Merge branch 'main' into main
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240702@sha256:5d675b67826ac643ee64ecf2ef78adac1e491eef9a845f30818a1c0d1338ecc8 as dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240708@sha256:2a9e3231c34493cb861299d475c84c031e7f04519dbc895bbebb5017d479a3cb as dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -41,7 +41,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240702@sha256:419a873052cf2f012ed1977e4a771a38e68ce64ea1c66047cc06232b1a79bafe
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240708@sha256:0af3a5bb036c9a4b6a5a51becaa6e94fe182f6bc97480d57e8f2e6f994bfa453
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
@@ -50,7 +50,7 @@ ENV NODE_ENV=production \
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/dist ./dist
|
||||
COPY --from=prod /usr/src/app/bin ./bin
|
||||
COPY --from=web /usr/src/app/build ./www
|
||||
COPY --from=web /usr/src/app/build /build/www
|
||||
COPY server/resources resources
|
||||
COPY server/package.json server/package-lock.json ./
|
||||
COPY server/start*.sh ./
|
||||
|
||||
3066
server/package-lock.json
generated
3066
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@
|
||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.52.0",
|
||||
"@opentelemetry/sdk-node": "^0.52.0",
|
||||
"@react-email/components": "^0.0.19",
|
||||
"@react-email/components": "^0.0.21",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
@@ -121,7 +121,7 @@
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"rimraf": "^5.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sql-formatter": "^15.0.0",
|
||||
|
||||
@@ -27,13 +27,28 @@ export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
|
||||
const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283';
|
||||
export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;
|
||||
|
||||
const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources';
|
||||
|
||||
export const citiesFile = 'cities500.txt';
|
||||
export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
|
||||
export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
|
||||
export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
|
||||
export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
|
||||
|
||||
const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
|
||||
|
||||
const folders = {
|
||||
geodata: join(buildFolder, 'geodata'),
|
||||
web: join(buildFolder, 'www'),
|
||||
};
|
||||
|
||||
export const resourcePaths = {
|
||||
lockFile: join(buildFolder, 'build-lock.json'),
|
||||
geodata: {
|
||||
dateFile: join(folders.geodata, 'geodata-date.txt'),
|
||||
admin1: join(folders.geodata, 'admin1CodesASCII.txt'),
|
||||
admin2: join(folders.geodata, 'admin2Codes.txt'),
|
||||
cities500: join(folders.geodata, citiesFile),
|
||||
},
|
||||
web: {
|
||||
root: folders.web,
|
||||
indexHtml: join(folders.web, 'index.html'),
|
||||
},
|
||||
};
|
||||
|
||||
export const MOBILE_REDIRECT = 'app.immich:/';
|
||||
export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { ApiQuery, ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { PartnerService } from 'src/services/partner.service';
|
||||
@@ -16,8 +16,8 @@ export class PartnerController {
|
||||
@ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true })
|
||||
@Authenticated()
|
||||
// TODO: remove 'direction' and convert to full query dto
|
||||
getPartners(@Auth() auth: AuthDto, @Query('direction') direction: PartnerDirection): Promise<PartnerResponseDto[]> {
|
||||
return this.service.getAll(auth, direction);
|
||||
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||
return this.service.search(auth, dto);
|
||||
}
|
||||
|
||||
@Post(':id')
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { PartnerDirection } from 'src/interfaces/partner.interface';
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsNotEmpty()
|
||||
inTimeline!: boolean;
|
||||
}
|
||||
|
||||
export class PartnerSearchDto {
|
||||
@IsEnum(PartnerDirection)
|
||||
@ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' })
|
||||
direction!: PartnerDirection;
|
||||
}
|
||||
|
||||
export class PartnerResponseDto extends UserResponseDto {
|
||||
inTimeline?: boolean;
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ export class UserAdminResponseDto extends UserResponseDto {
|
||||
}
|
||||
|
||||
export function mapUserAdmin(entity: UserEntity): UserAdminResponseDto {
|
||||
const license = entity.metadata.find(
|
||||
const license = entity.metadata?.find(
|
||||
(item): item is UserMetadataEntity<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE,
|
||||
)?.value;
|
||||
return {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getName } from 'i18n-iso-countries';
|
||||
import { createReadStream, existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import readLine from 'node:readline';
|
||||
import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants';
|
||||
import { citiesFile, resourcePaths } from 'src/constants';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
@@ -37,7 +37,7 @@ export class MapRepository implements IMapRepository {
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.logger.log('Initializing metadata repository');
|
||||
const geodataDate = await readFile(geodataDatePath, 'utf8');
|
||||
const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8');
|
||||
|
||||
// TODO move to service init
|
||||
const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE);
|
||||
@@ -150,8 +150,8 @@ export class MapRepository implements IMapRepository {
|
||||
const queryRunner = this.dataSource.createQueryRunner();
|
||||
await queryRunner.connect();
|
||||
|
||||
const admin1 = await this.loadAdmin(geodataAdmin1Path);
|
||||
const admin2 = await this.loadAdmin(geodataAdmin2Path);
|
||||
const admin1 = await this.loadAdmin(resourcePaths.geodata.admin1);
|
||||
const admin2 = await this.loadAdmin(resourcePaths.geodata.admin2);
|
||||
|
||||
try {
|
||||
await queryRunner.startTransaction();
|
||||
@@ -221,7 +221,7 @@ export class MapRepository implements IMapRepository {
|
||||
admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
|
||||
admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
|
||||
}),
|
||||
geodataCities500Path,
|
||||
resourcePaths.geodata.cities500,
|
||||
{ entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { resourcePaths } from 'src/constants';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
@@ -61,9 +62,9 @@ export class ServerInfoRepository implements IServerInfoRepository {
|
||||
maybeFirstLine('convert --version'),
|
||||
]);
|
||||
|
||||
const lockfile = await readFile('build-lock.json')
|
||||
const lockfile = await readFile(resourcePaths.lockFile)
|
||||
.then((buffer) => JSON.parse(buffer.toString()))
|
||||
.catch(() => this.logger.warn('Failed to read build-lock.json'));
|
||||
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
|
||||
|
||||
return {
|
||||
nodejs: nodejsOutput || process.env.NODE_VERSION || '',
|
||||
|
||||
@@ -202,7 +202,7 @@ describe(StorageRepository.name, () => {
|
||||
.filter((entry) => entry[1])
|
||||
.map(([file]) => file);
|
||||
|
||||
expect(actual.sort()).toEqual(expected.sort());
|
||||
expect(actual.toSorted()).toEqual(expected.toSorted());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,8 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression, Interval } from '@nestjs/schedule';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { ONE_HOUR, WEB_ROOT } from 'src/constants';
|
||||
import { ONE_HOUR, resourcePaths } from 'src/constants';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AuthService } from 'src/services/auth.service';
|
||||
import { JobService } from 'src/services/job.service';
|
||||
@@ -56,9 +55,9 @@ export class ApiService {
|
||||
ssr(excludePaths: string[]) {
|
||||
let index = '';
|
||||
try {
|
||||
index = readFileSync(join(WEB_ROOT, 'index.html')).toString();
|
||||
index = readFileSync(resourcePaths.web.indexHtml).toString();
|
||||
} catch {
|
||||
this.logger.warn('Unable to open `www/index.html, skipping SSR.');
|
||||
this.logger.warn(`Unable to open ${resourcePaths.web.indexHtml}, skipping SSR.`);
|
||||
}
|
||||
|
||||
return async (request: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
@@ -255,13 +255,11 @@ describe(AssetMediaService.name, () => {
|
||||
}
|
||||
|
||||
it('should be sorted (valid)', () => {
|
||||
// TODO: use toSorted in NodeJS 20.
|
||||
expect(valid).toEqual([...valid].sort());
|
||||
expect(valid).toEqual(valid.toSorted());
|
||||
});
|
||||
|
||||
it('should be sorted (invalid)', () => {
|
||||
// TODO: use toSorted in NodeJS 20.
|
||||
expect(invalid).toEqual([...invalid].sort());
|
||||
expect(invalid).toEqual(invalid.toSorted());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,16 +21,16 @@ describe(PartnerService.name, () => {
|
||||
expect(sut).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
describe('search', () => {
|
||||
it("should return a list of partners with whom I've shared my library", async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedBy)).resolves.toBeDefined();
|
||||
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedBy })).resolves.toBeDefined();
|
||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
|
||||
it('should return a list of partners who have shared their libraries with me', async () => {
|
||||
partnerMock.getAll.mockResolvedValue([partnerStub.adminToUser1, partnerStub.user1ToAdmin1]);
|
||||
await expect(sut.getAll(authStub.user1, PartnerDirection.SharedWith)).resolves.toBeDefined();
|
||||
await expect(sut.search(authStub.user1, { direction: PartnerDirection.SharedWith })).resolves.toBeDefined();
|
||||
expect(partnerMock.getAll).toHaveBeenCalledWith(authStub.user1.user.id);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
|
||||
import { mapUser } from 'src/dtos/user.dto';
|
||||
import { PartnerEntity } from 'src/entities/partner.entity';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
@@ -38,7 +38,7 @@ export class PartnerService {
|
||||
await this.repository.remove(partner);
|
||||
}
|
||||
|
||||
async getAll(auth: AuthDto, direction: PartnerDirection): Promise<PartnerResponseDto[]> {
|
||||
async search(auth: AuthDto, { direction }: PartnerSearchDto): Promise<PartnerResponseDto[]> {
|
||||
const partners = await this.repository.getAll(auth.user.id);
|
||||
const key = direction === PartnerDirection.SharedBy ? 'sharedById' : 'sharedWithId';
|
||||
return partners
|
||||
|
||||
@@ -145,7 +145,7 @@ describe('mimeTypes', () => {
|
||||
|
||||
it('should be a sorted list', () => {
|
||||
const keys = Object.keys(mimeTypes.video);
|
||||
expect(keys).toEqual([...keys].sort());
|
||||
expect(keys).toEqual(keys.toSorted());
|
||||
});
|
||||
|
||||
it('should contain only video mime types', () => {
|
||||
@@ -171,7 +171,7 @@ describe('mimeTypes', () => {
|
||||
|
||||
it('should be a sorted list', () => {
|
||||
const keys = Object.keys(mimeTypes.sidecar);
|
||||
expect(keys).toEqual([...keys].sort());
|
||||
expect(keys).toEqual(keys.toSorted());
|
||||
});
|
||||
|
||||
it('should contain only xml mime types', () => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser';
|
||||
import { existsSync } from 'node:fs';
|
||||
import sirv from 'sirv';
|
||||
import { ApiModule } from 'src/app.module';
|
||||
import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/constants';
|
||||
import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/constants';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { ApiService } from 'src/services/api.service';
|
||||
@@ -38,11 +38,11 @@ async function bootstrap() {
|
||||
useSwagger(app);
|
||||
|
||||
app.setGlobalPrefix('api', { exclude: excludePaths });
|
||||
if (existsSync(WEB_ROOT)) {
|
||||
if (existsSync(resourcePaths.web.root)) {
|
||||
// copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
|
||||
// provides serving of precompressed assets and caching of immutable assets
|
||||
app.use(
|
||||
sirv(WEB_ROOT, {
|
||||
sirv(resourcePaths.web.root, {
|
||||
etag: true,
|
||||
gzip: true,
|
||||
brotli: true,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "node16",
|
||||
"lib": ["dom", "es2023"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
Reference in New Issue
Block a user