mirror of
https://github.com/immich-app/immich.git
synced 2024-12-24 10:37:28 +02:00
feat(server): Add support for client-side hashing (#2072)
* Modify controller DTOs * Can check duplicates on server side * Remove deviceassetid and deviceid * Remove device ids from file uploader * Add db migration for removed device ids * Don't sanitize checksum * Convert asset checksum to string * Make checksum not optional for asset * Use enums when rejecting duplicates * Cleanup * Return of the device id, but optional * Don't use deviceId for upload folder * Use checksum in thumb path * Only use asset id in thumb path * Openapi generation * Put deviceAssetId back in asset response dto * Add missing checksum in test fixture * Add another missing checksum in test fixture * Cleanup asset repository * Add back previous /exists endpoint * Require checksum to not be null * Correctly set deviceId in db * Remove index * Fix compilation errors * Make device id nullabel in asset response dto * Reduce PR scope * Revert asset service * Reorder imports * Reorder imports * Reduce PR scope * Reduce PR scope * Reduce PR scope * Reduce PR scope * Reduce PR scope * Update openapi * Reduce PR scope * refactor: asset bulk upload check * chore: regenreate open-api * chore: fix tests * chore: tests * update migrations and regenerate api * Feat: use checksum in web file uploader * Change to wasm-crypto * Use crypto api for checksumming in web uploader * Minor cleanup of file upload * feat(web): pause and resume jobs * Make device asset id not nullable again * Cleanup * Device id not nullable in response dto * Update API specs * Bump api specs * Remove old TODO comment * Remove NOT NULL constraint on checksum index * Fix requested pubspec changes * Remove unneeded import * Update server/apps/immich/src/api-v1/asset/asset.service.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Update server/apps/immich/src/api-v1/asset/asset-repository.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Remove unneeded check * Update server/apps/immich/src/api-v1/asset/asset-repository.ts Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Remove hashing in the web uploader * Cleanup file uploader * Remove varchar from asset entity fields * Return 200 from bulk upload check * Put device asset id back into asset repository * Merge migrations * Revert pubspec lock * Update openapi specs * Merge upstream changes * Fix failing asset service tests * Fix formatting issue * Cleanup migrations * Remove newline from pubspec * Revert newline * Checkout main version * Revert again * Only return AssetCheck --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
This commit is contained in:
parent
49b74e9091
commit
1b54c4f8e7
12
mobile/openapi/.openapi-generator/FILES
generated
12
mobile/openapi/.openapi-generator/FILES
generated
@ -17,6 +17,10 @@ doc/AlbumCountResponseDto.md
|
||||
doc/AlbumResponseDto.md
|
||||
doc/AllJobStatusResponseDto.md
|
||||
doc/AssetApi.md
|
||||
doc/AssetBulkUploadCheckDto.md
|
||||
doc/AssetBulkUploadCheckItem.md
|
||||
doc/AssetBulkUploadCheckResponseDto.md
|
||||
doc/AssetBulkUploadCheckResult.md
|
||||
doc/AssetCountByTimeBucket.md
|
||||
doc/AssetCountByTimeBucketResponseDto.md
|
||||
doc/AssetCountByUserIdResponseDto.md
|
||||
@ -142,6 +146,10 @@ lib/model/api_key_create_dto.dart
|
||||
lib/model/api_key_create_response_dto.dart
|
||||
lib/model/api_key_response_dto.dart
|
||||
lib/model/api_key_update_dto.dart
|
||||
lib/model/asset_bulk_upload_check_dto.dart
|
||||
lib/model/asset_bulk_upload_check_item.dart
|
||||
lib/model/asset_bulk_upload_check_response_dto.dart
|
||||
lib/model/asset_bulk_upload_check_result.dart
|
||||
lib/model/asset_count_by_time_bucket.dart
|
||||
lib/model/asset_count_by_time_bucket_response_dto.dart
|
||||
lib/model/asset_count_by_user_id_response_dto.dart
|
||||
@ -236,6 +244,10 @@ test/api_key_create_response_dto_test.dart
|
||||
test/api_key_response_dto_test.dart
|
||||
test/api_key_update_dto_test.dart
|
||||
test/asset_api_test.dart
|
||||
test/asset_bulk_upload_check_dto_test.dart
|
||||
test/asset_bulk_upload_check_item_test.dart
|
||||
test/asset_bulk_upload_check_response_dto_test.dart
|
||||
test/asset_bulk_upload_check_result_test.dart
|
||||
test/asset_count_by_time_bucket_response_dto_test.dart
|
||||
test/asset_count_by_time_bucket_test.dart
|
||||
test/asset_count_by_user_id_response_dto_test.dart
|
||||
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetBulkUploadCheckDto.md
generated
Normal file
BIN
mobile/openapi/doc/AssetBulkUploadCheckDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/AssetBulkUploadCheckItem.md
generated
Normal file
BIN
mobile/openapi/doc/AssetBulkUploadCheckItem.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/AssetBulkUploadCheckResponseDto.md
generated
Normal file
BIN
mobile/openapi/doc/AssetBulkUploadCheckResponseDto.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/doc/AssetBulkUploadCheckResult.md
generated
Normal file
BIN
mobile/openapi/doc/AssetBulkUploadCheckResult.md
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_item.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_item.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_result.dart
generated
Normal file
BIN
mobile/openapi/lib/model/asset_bulk_upload_check_result.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_bulk_upload_check_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_bulk_upload_check_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_bulk_upload_check_item_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_bulk_upload_check_item_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_bulk_upload_check_response_dto_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_bulk_upload_check_response_dto_test.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/test/asset_bulk_upload_check_result_test.dart
generated
Normal file
BIN
mobile/openapi/test/asset_bulk_upload_check_result_test.dart
generated
Normal file
Binary file not shown.
@ -10,13 +10,17 @@ import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||
import { In } from 'typeorm/find-options/operator/In';
|
||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||
import { ITagRepository } from '../tag/tag.repository';
|
||||
import { IsNull, Not } from 'typeorm';
|
||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
|
||||
export interface AssetCheck {
|
||||
id: string;
|
||||
checksum: Buffer;
|
||||
}
|
||||
|
||||
export interface IAssetRepository {
|
||||
get(id: string): Promise<AssetEntity | null>;
|
||||
create(
|
||||
@ -38,11 +42,8 @@ export interface IAssetRepository {
|
||||
getAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||
getArchivedAssetCountByUserId(userId: string): Promise<AssetCountByUserIdResponseDto>;
|
||||
getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise<AssetEntity[]>;
|
||||
getAssetByChecksum(userId: string, checksum: Buffer): Promise<AssetEntity>;
|
||||
getExistingAssets(
|
||||
userId: string,
|
||||
checkDuplicateAssetDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto>;
|
||||
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
||||
countByIdAndUser(assetId: string, userId: string): Promise<number>;
|
||||
}
|
||||
|
||||
@ -310,41 +311,39 @@ export class AssetRepository implements IAssetRepository {
|
||||
* @returns Promise<string[]> - Array of assetIds belong to the device
|
||||
*/
|
||||
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
|
||||
const rows = await this.assetRepository.find({
|
||||
const items = await this.assetRepository.find({
|
||||
select: { deviceAssetId: true },
|
||||
where: {
|
||||
ownerId,
|
||||
deviceId,
|
||||
isVisible: true,
|
||||
},
|
||||
select: ['deviceAssetId'],
|
||||
});
|
||||
const res: string[] = [];
|
||||
rows.forEach((v) => res.push(v.deviceAssetId));
|
||||
|
||||
return res;
|
||||
return items.map((asset) => asset.deviceAssetId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset by checksum on the database
|
||||
* Get assets by checksums on the database
|
||||
* @param ownerId
|
||||
* @param checksum
|
||||
* @param checksums
|
||||
*
|
||||
*/
|
||||
getAssetByChecksum(ownerId: string, checksum: Buffer): Promise<AssetEntity> {
|
||||
return this.assetRepository.findOneOrFail({
|
||||
async getAssetsByChecksums(ownerId: string, checksums: Buffer[]): Promise<AssetCheck[]> {
|
||||
return this.assetRepository.find({
|
||||
select: {
|
||||
id: true,
|
||||
checksum: true,
|
||||
},
|
||||
where: {
|
||||
ownerId,
|
||||
checksum,
|
||||
checksum: In(checksums),
|
||||
},
|
||||
relations: ['exifInfo'],
|
||||
});
|
||||
}
|
||||
|
||||
async getExistingAssets(
|
||||
ownerId: string,
|
||||
checkDuplicateAssetDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
const existingAssets = await this.assetRepository.find({
|
||||
async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]> {
|
||||
const assets = await this.assetRepository.find({
|
||||
select: { deviceAssetId: true },
|
||||
where: {
|
||||
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
|
||||
@ -352,7 +351,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
ownerId,
|
||||
},
|
||||
});
|
||||
return new CheckExistingAssetsResponseDto(existingAssets.map((a) => a.deviceAssetId));
|
||||
return assets.map((asset) => asset.deviceAssetId);
|
||||
}
|
||||
|
||||
async countByIdAndUser(assetId: string, ownerId: string): Promise<number> {
|
||||
|
@ -57,6 +57,8 @@ import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
||||
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
|
||||
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
|
||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||
import { AssetBulkUploadCheckResponseDto } from './response-dto/asset-check-response.dto';
|
||||
import { AssetIdDto } from './dto/asset-id.dto';
|
||||
import { DeviceIdDto } from './dto/device-id.dto';
|
||||
|
||||
@ -332,6 +334,19 @@ export class AssetController {
|
||||
return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
*/
|
||||
@Authenticated()
|
||||
@Post('/bulk-upload-check')
|
||||
@HttpCode(200)
|
||||
bulkUploadCheck(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) dto: AssetBulkUploadCheckDto,
|
||||
): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
return this.assetService.bulkUploadCheck(authUser, dto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post('/shared-link')
|
||||
async createAssetsSharedLink(
|
||||
|
@ -17,7 +17,7 @@ export class AssetCore {
|
||||
owner: { id: authUser.id } as UserEntity,
|
||||
|
||||
mimeType: file.mimeType,
|
||||
checksum: file.checksum || null,
|
||||
checksum: file.checksum,
|
||||
originalPath: file.originalPath,
|
||||
|
||||
deviceAssetId: dto.deviceAssetId,
|
||||
|
@ -157,7 +157,7 @@ describe('AssetService', () => {
|
||||
getLocationsByUserId: jest.fn(),
|
||||
getSearchPropertiesByUserId: jest.fn(),
|
||||
getAssetByTimeBucket: jest.fn(),
|
||||
getAssetByChecksum: jest.fn(),
|
||||
getAssetsByChecksums: jest.fn(),
|
||||
getAssetCountByUserId: jest.fn(),
|
||||
getArchivedAssetCountByUserId: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
@ -299,7 +299,7 @@ describe('AssetService', () => {
|
||||
(error as any).constraint = 'UQ_userid_checksum';
|
||||
|
||||
assetRepositoryMock.create.mockRejectedValue(error);
|
||||
assetRepositoryMock.getAssetByChecksum.mockResolvedValue(_getAsset_1());
|
||||
assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
||||
|
||||
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
|
||||
|
||||
|
@ -63,6 +63,12 @@ import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
|
||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
import { AddAssetsDto } from '../album/dto/add-assets.dto';
|
||||
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
|
||||
import { AssetBulkUploadCheckDto } from './dto/asset-check.dto';
|
||||
import {
|
||||
AssetUploadAction,
|
||||
AssetRejectReason,
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
} from './response-dto/asset-check-response.dto';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@ -128,7 +134,8 @@ export class AssetService {
|
||||
|
||||
// handle duplicates with a success response
|
||||
if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_userid_checksum') {
|
||||
const duplicate = await this.getAssetByChecksum(authUser.id, file.checksum);
|
||||
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum);
|
||||
const [duplicate] = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
|
||||
return { id: duplicate.id, duplicate: true };
|
||||
}
|
||||
|
||||
@ -463,7 +470,40 @@ export class AssetService {
|
||||
authUser: AuthUserDto,
|
||||
checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto);
|
||||
return {
|
||||
existingIds: await this._assetRepository.getExistingAssets(authUser.id, checkExistingAssetsDto),
|
||||
};
|
||||
}
|
||||
|
||||
async bulkUploadCheck(authUser: AuthUserDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
|
||||
const results = await this._assetRepository.getAssetsByChecksums(authUser.id, checksums);
|
||||
const resultsMap: Record<string, string> = {};
|
||||
|
||||
for (const { id, checksum } of results) {
|
||||
resultsMap[checksum.toString('hex')] = id;
|
||||
}
|
||||
|
||||
return {
|
||||
results: dto.assets.map(({ id, checksum }) => {
|
||||
const duplicate = resultsMap[checksum];
|
||||
if (duplicate) {
|
||||
return {
|
||||
id,
|
||||
assetId: duplicate,
|
||||
action: AssetUploadAction.REJECT,
|
||||
reason: AssetRejectReason.DUPLICATE,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO mime-check
|
||||
|
||||
return {
|
||||
id,
|
||||
action: AssetUploadAction.ACCEPT,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async getAssetCountByTimeBucket(
|
||||
@ -482,10 +522,6 @@ export class AssetService {
|
||||
return mapAssetCountByTimeBucket(result);
|
||||
}
|
||||
|
||||
getAssetByChecksum(userId: string, checksum: Buffer) {
|
||||
return this._assetRepository.getAssetByChecksum(userId, checksum);
|
||||
}
|
||||
|
||||
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this._assetRepository.getAssetCountByUserId(authUser.id);
|
||||
}
|
||||
|
19
server/apps/immich/src/api-v1/asset/dto/asset-check.dto.ts
Normal file
19
server/apps/immich/src/api-v1/asset/dto/asset-check.dto.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class AssetBulkUploadCheckItem {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
checksum!: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetBulkUploadCheckItem)
|
||||
assets!: AssetBulkUploadCheckItem[];
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
export class AssetBulkUploadCheckResult {
|
||||
id!: string;
|
||||
action!: AssetUploadAction;
|
||||
reason?: AssetRejectReason;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResponseDto {
|
||||
results!: AssetBulkUploadCheckResult[];
|
||||
}
|
||||
|
||||
export enum AssetUploadAction {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
}
|
||||
|
||||
export enum AssetRejectReason {
|
||||
DUPLICATE = 'duplicate',
|
||||
UNSUPPORTED_FORMAT = 'unsupported-format',
|
||||
}
|
@ -1,6 +1,3 @@
|
||||
export class CheckExistingAssetsResponseDto {
|
||||
constructor(existingIds: string[]) {
|
||||
this.existingIds = existingIds;
|
||||
}
|
||||
existingIds: string[];
|
||||
existingIds!: string[];
|
||||
}
|
||||
|
@ -3251,6 +3251,49 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/bulk-upload-check": {
|
||||
"post": {
|
||||
"operationId": "bulkUploadCheck",
|
||||
"description": "Checks if assets exist by checksums",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetBulkUploadCheckDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetBulkUploadCheckResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Asset"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/shared-link": {
|
||||
"post": {
|
||||
"operationId": "createAssetsSharedLink",
|
||||
@ -6046,6 +6089,78 @@
|
||||
"existingIds"
|
||||
]
|
||||
},
|
||||
"AssetBulkUploadCheckItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"checksum": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"checksum"
|
||||
]
|
||||
},
|
||||
"AssetBulkUploadCheckDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetBulkUploadCheckItem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assets"
|
||||
]
|
||||
},
|
||||
"AssetBulkUploadCheckResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"accept",
|
||||
"reject"
|
||||
]
|
||||
},
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"duplicate",
|
||||
"unsupported-format"
|
||||
]
|
||||
},
|
||||
"assetId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"action"
|
||||
]
|
||||
},
|
||||
"AssetBulkUploadCheckResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"results": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetBulkUploadCheckResult"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"results"
|
||||
]
|
||||
},
|
||||
"CreateAssetsShareLinkDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -147,6 +147,7 @@ export const assetEntityStub = {
|
||||
deviceId: 'device-id',
|
||||
originalPath: 'upload/upload/path.ext',
|
||||
resizePath: null,
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.IMAGE,
|
||||
webpPath: null,
|
||||
encodedVideoPath: null,
|
||||
@ -173,6 +174,7 @@ export const assetEntityStub = {
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path.ext',
|
||||
resizePath: '/uploads/user-id/thumbs/path.ext',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.IMAGE,
|
||||
webpPath: null,
|
||||
encodedVideoPath: null,
|
||||
@ -201,6 +203,7 @@ export const assetEntityStub = {
|
||||
deviceId: 'device-id',
|
||||
originalPath: '/original/path.ext',
|
||||
resizePath: '/uploads/user-id/thumbs/path.ext',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
type: AssetType.VIDEO,
|
||||
webpPath: null,
|
||||
encodedVideoPath: null,
|
||||
@ -246,6 +249,7 @@ export const assetEntityStub = {
|
||||
owner: userEntityStub.user1,
|
||||
ownerId: 'user-id',
|
||||
deviceId: 'device-id',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
originalPath: '/original/path.ext',
|
||||
resizePath: '/uploads/user-id/thumbs/path.ext',
|
||||
type: AssetType.IMAGE,
|
||||
@ -663,6 +667,7 @@ export const sharedLinkStub = {
|
||||
type: AssetType.VIDEO,
|
||||
originalPath: 'fake_path/jpeg',
|
||||
resizePath: '',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
fileModifiedAt: today.toISOString(),
|
||||
fileCreatedAt: today.toISOString(),
|
||||
createdAt: today.toISOString(),
|
||||
|
@ -75,9 +75,9 @@ export class AssetEntity {
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
mimeType!: string | null;
|
||||
|
||||
@Column({ type: 'bytea', nullable: true, select: false })
|
||||
@Index({ where: `'checksum' IS NOT NULL` }) // avoid null index
|
||||
checksum?: Buffer | null; // sha1 checksum
|
||||
@Column({ type: 'bytea' })
|
||||
@Index()
|
||||
checksum!: Buffer; // sha1 checksum
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
duration!: string | null;
|
||||
|
@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RequireChecksumNotNull1684328185099 implements MigrationInterface {
|
||||
name = 'removeNotNullFromChecksumIndex1684328185099';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" SET NOT NULL`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum") `);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_8d3efe36c0755849395e6ea866"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" DROP NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE ('checksum' IS NOT NULL)`,
|
||||
);
|
||||
}
|
||||
}
|
164
web/src/api/open-api/api.ts
generated
164
web/src/api/open-api/api.ts
generated
@ -346,6 +346,96 @@ export interface AllJobStatusResponseDto {
|
||||
*/
|
||||
'recognize-faces-queue': JobStatusDto;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetBulkUploadCheckDto
|
||||
*/
|
||||
export interface AssetBulkUploadCheckDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetBulkUploadCheckItem>}
|
||||
* @memberof AssetBulkUploadCheckDto
|
||||
*/
|
||||
'assets': Array<AssetBulkUploadCheckItem>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetBulkUploadCheckItem
|
||||
*/
|
||||
export interface AssetBulkUploadCheckItem {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetBulkUploadCheckItem
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetBulkUploadCheckItem
|
||||
*/
|
||||
'checksum': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetBulkUploadCheckResponseDto
|
||||
*/
|
||||
export interface AssetBulkUploadCheckResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetBulkUploadCheckResult>}
|
||||
* @memberof AssetBulkUploadCheckResponseDto
|
||||
*/
|
||||
'results': Array<AssetBulkUploadCheckResult>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetBulkUploadCheckResult
|
||||
*/
|
||||
export interface AssetBulkUploadCheckResult {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetBulkUploadCheckResult
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetBulkUploadCheckResult
|
||||
*/
|
||||
'action': AssetBulkUploadCheckResultActionEnum;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetBulkUploadCheckResult
|
||||
*/
|
||||
'reason'?: AssetBulkUploadCheckResultReasonEnum;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetBulkUploadCheckResult
|
||||
*/
|
||||
'assetId'?: string;
|
||||
}
|
||||
|
||||
export const AssetBulkUploadCheckResultActionEnum = {
|
||||
Accept: 'accept',
|
||||
Reject: 'reject'
|
||||
} as const;
|
||||
|
||||
export type AssetBulkUploadCheckResultActionEnum = typeof AssetBulkUploadCheckResultActionEnum[keyof typeof AssetBulkUploadCheckResultActionEnum];
|
||||
export const AssetBulkUploadCheckResultReasonEnum = {
|
||||
Duplicate: 'duplicate',
|
||||
UnsupportedFormat: 'unsupported-format'
|
||||
} as const;
|
||||
|
||||
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@ -4120,6 +4210,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
* @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
bulkUploadCheck: async (assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'assetBulkUploadCheckDto' is not null or undefined
|
||||
assertParamExists('bulkUploadCheck', 'assetBulkUploadCheckDto', assetBulkUploadCheckDto)
|
||||
const localVarPath = `/asset/bulk-upload-check`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(assetBulkUploadCheckDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@ -5312,6 +5446,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, key, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
* @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async bulkUploadCheck(assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetBulkUploadCheckResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.bulkUploadCheck(assetBulkUploadCheckDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@ -5595,6 +5739,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
addAssetsToSharedLink(addAssetsDto: AddAssetsDto, key?: string, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.addAssetsToSharedLink(addAssetsDto, key, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
* @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
bulkUploadCheck(assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options?: any): AxiosPromise<AssetBulkUploadCheckResponseDto> {
|
||||
return localVarFp.bulkUploadCheck(assetBulkUploadCheckDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@ -5856,6 +6009,17 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).addAssetsToSharedLink(addAssetsDto, key, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
* @param {AssetBulkUploadCheckDto} assetBulkUploadCheckDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public bulkUploadCheck(assetBulkUploadCheckDto: AssetBulkUploadCheckDto, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).bulkUploadCheck(assetBulkUploadCheckDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
|
@ -4,7 +4,7 @@ import {
|
||||
} from './../components/shared-components/notification/notification';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import type { UploadAsset } from '../models/upload-asset';
|
||||
import { api, AssetFileUploadResponseDto } from '@api';
|
||||
import { AssetFileUploadResponseDto } from '@api';
|
||||
import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils';
|
||||
import { mergeMap, filter, firstValueFrom, from, of, combineLatestAll } from 'rxjs';
|
||||
import axios from 'axios';
|
||||
@ -73,7 +73,7 @@ async function fileUploader(
|
||||
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
|
||||
|
||||
try {
|
||||
// Create and add Unique ID of asset on the device
|
||||
// Create and add pseudo-unique ID of asset on the device
|
||||
formData.append('deviceAssetId', deviceAssetId);
|
||||
|
||||
// Get device id - for web -> use WEB
|
||||
@ -102,23 +102,6 @@ async function fileUploader(
|
||||
// failed uploads.
|
||||
formData.append('assetData', new File([asset], asset.name, { type: mimeType }));
|
||||
|
||||
// Check if asset upload on server before performing upload
|
||||
const { data, status } = await api.assetApi.checkDuplicateAsset(
|
||||
{
|
||||
deviceAssetId: String(deviceAssetId),
|
||||
deviceId: 'WEB'
|
||||
},
|
||||
sharedKey
|
||||
);
|
||||
|
||||
if (status === 200 && data.isExist && data.id) {
|
||||
if (albumId) {
|
||||
await addAssetsToAlbum(albumId, [data.id], sharedKey);
|
||||
}
|
||||
|
||||
return data.id;
|
||||
}
|
||||
|
||||
const newUploadAsset: UploadAsset = {
|
||||
id: deviceAssetId,
|
||||
file: asset,
|
||||
|
Loading…
Reference in New Issue
Block a user