2023-02-25 15:12:03 +01:00
|
|
|
import { when } from 'jest-when';
|
2024-03-21 14:07:47 +01:00
|
|
|
import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto';
|
|
|
|
import { CreateAssetDto } from 'src/dtos/asset-v1.dto';
|
2024-03-20 22:02:51 +01:00
|
|
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
|
|
|
import { ExifEntity } from 'src/entities/exif.entity';
|
2024-03-21 14:07:47 +01:00
|
|
|
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
|
|
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
|
|
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
|
|
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|
|
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
2024-03-21 14:07:47 +01:00
|
|
|
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { assetStub } from 'test/fixtures/asset.stub';
|
|
|
|
import { authStub } from 'test/fixtures/auth.stub';
|
|
|
|
import { fileStub } from 'test/fixtures/file.stub';
|
|
|
|
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
|
|
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
|
|
|
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
|
|
|
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
|
|
|
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
|
|
|
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
2023-11-17 05:24:31 +01:00
|
|
|
import { QueryFailedError } from 'typeorm';
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
const _getCreateAssetDto = (): CreateAssetDto => {
|
|
|
|
const createAssetDto = new CreateAssetDto();
|
|
|
|
createAssetDto.deviceAssetId = 'deviceAssetId';
|
|
|
|
createAssetDto.deviceId = 'deviceId';
|
2023-05-29 16:05:14 +02:00
|
|
|
createAssetDto.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
|
|
|
|
createAssetDto.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
|
2023-01-30 17:14:13 +01:00
|
|
|
createAssetDto.isFavorite = false;
|
2023-04-12 17:37:52 +02:00
|
|
|
createAssetDto.isArchived = false;
|
2023-01-30 17:14:13 +01:00
|
|
|
createAssetDto.duration = '0:00:00.000000';
|
2023-09-20 13:16:33 +02:00
|
|
|
createAssetDto.libraryId = 'libraryId';
|
2023-01-30 17:14:13 +01:00
|
|
|
|
|
|
|
return createAssetDto;
|
|
|
|
};
|
|
|
|
|
|
|
|
const _getAsset_1 = () => {
|
|
|
|
const asset_1 = new AssetEntity();
|
|
|
|
|
|
|
|
asset_1.id = 'id_1';
|
2023-02-19 17:44:53 +01:00
|
|
|
asset_1.ownerId = 'user_id_1';
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_1.deviceAssetId = 'device_asset_id_1';
|
|
|
|
asset_1.deviceId = 'device_id_1';
|
|
|
|
asset_1.type = AssetType.VIDEO;
|
|
|
|
asset_1.originalPath = 'fake_path/asset_1.jpeg';
|
|
|
|
asset_1.resizePath = '';
|
2023-05-29 16:05:14 +02:00
|
|
|
asset_1.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
|
|
|
|
asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
|
|
|
|
asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z');
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_1.isFavorite = false;
|
2023-04-12 17:37:52 +02:00
|
|
|
asset_1.isArchived = false;
|
2023-01-30 17:14:13 +01:00
|
|
|
asset_1.webpPath = '';
|
|
|
|
asset_1.encodedVideoPath = '';
|
|
|
|
asset_1.duration = '0:00:00.000000';
|
2023-05-06 03:33:30 +02:00
|
|
|
asset_1.exifInfo = new ExifEntity();
|
2024-02-02 04:18:00 +01:00
|
|
|
asset_1.exifInfo.latitude = 49.533_547;
|
|
|
|
asset_1.exifInfo.longitude = 10.703_075;
|
2023-01-30 17:14:13 +01:00
|
|
|
return asset_1;
|
|
|
|
};
|
|
|
|
|
2022-08-27 07:53:37 +02:00
|
|
|
describe('AssetService', () => {
|
2024-03-21 14:07:47 +01:00
|
|
|
let sut: AssetServiceV1;
|
2023-06-28 15:56:24 +02:00
|
|
|
let accessMock: IAccessRepositoryMock;
|
2024-01-25 18:52:21 +01:00
|
|
|
let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
|
|
|
|
let assetMock: jest.Mocked<IAssetRepository>;
|
2023-01-22 05:13:36 +01:00
|
|
|
let jobMock: jest.Mocked<IJobRepository>;
|
2023-09-20 13:16:33 +02:00
|
|
|
let libraryMock: jest.Mocked<ILibraryRepository>;
|
2024-02-12 05:40:34 +01:00
|
|
|
let storageMock: jest.Mocked<IStorageRepository>;
|
2024-01-13 01:43:36 +01:00
|
|
|
let userMock: jest.Mocked<IUserRepository>;
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
beforeEach(() => {
|
2024-01-25 18:52:21 +01:00
|
|
|
assetRepositoryMockV1 = {
|
2023-01-30 17:14:13 +01:00
|
|
|
get: jest.fn(),
|
2024-03-09 00:16:32 +01:00
|
|
|
getAllByUserId: jest.fn(),
|
2022-08-27 07:53:37 +02:00
|
|
|
getDetectedObjectsByUserId: jest.fn(),
|
|
|
|
getLocationsByUserId: jest.fn(),
|
|
|
|
getSearchPropertiesByUserId: jest.fn(),
|
2023-05-24 23:08:21 +02:00
|
|
|
getAssetsByChecksums: jest.fn(),
|
2022-10-25 16:51:03 +02:00
|
|
|
getExistingAssets: jest.fn(),
|
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload
* updated fixtures with new property
* if upload is 'read-only', ensure there is no existing asset at the designated originalPath
* added test for file import as well as detecting existing image at read-only destination location
* Added storage service test for a case where it should not move read-only assets
* upload doesn't need the read-only flag available, just importing
* default isReadOnly on import endpoint to true
* formatting fixes
* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation
* updated code to reflect changes in MR
* fixed read stream promise return type
* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates
* refactor: import asset
* chore: open api
* chore: tests
* Added externalPath support for individual users, updated UI to allow this to be set by admin
* added missing var for externalPath in ui
* chore: open api
* fix: compilation issues
* fix: server test
* built api, fixed user-response dto to include externalPath
* reverted accidental commit
* bad commit of duplicate externalPath in user response dto
* fixed tests to include externalPath on expected result
* fix: unit tests
* centralized supported filetypes, perform file type checking of asset and sidecar during file import process
* centralized supported filetype check method to keep regex DRY
* fixed typo
* combined migrations into one
* update api
* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not
* update mimetype
* Fixed detect correct mimetype
* revert asset-upload config
* reverted domain.constant
* refactor
* fix mime-type issue
* fix format
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
|
|
|
getByOriginalPath: jest.fn(),
|
2022-08-27 07:53:37 +02:00
|
|
|
};
|
|
|
|
|
2023-06-06 22:18:38 +02:00
|
|
|
accessMock = newAccessRepositoryMock();
|
2024-01-25 18:52:21 +01:00
|
|
|
assetMock = newAssetRepositoryMock();
|
2023-01-22 05:13:36 +01:00
|
|
|
jobMock = newJobRepositoryMock();
|
2023-09-20 13:16:33 +02:00
|
|
|
libraryMock = newLibraryRepositoryMock();
|
2024-02-12 05:40:34 +01:00
|
|
|
storageMock = newStorageRepositoryMock();
|
2024-01-13 01:43:36 +01:00
|
|
|
userMock = newUserRepositoryMock();
|
2023-01-22 05:13:36 +01:00
|
|
|
|
2024-03-21 14:07:47 +01:00
|
|
|
sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
|
2023-02-25 15:12:03 +01:00
|
|
|
|
2024-01-25 18:52:21 +01:00
|
|
|
when(assetRepositoryMockV1.get)
|
2023-08-01 03:28:07 +02:00
|
|
|
.calledWith(assetStub.livePhotoStillAsset.id)
|
|
|
|
.mockResolvedValue(assetStub.livePhotoStillAsset);
|
2024-01-25 18:52:21 +01:00
|
|
|
when(assetRepositoryMockV1.get)
|
2023-08-01 03:28:07 +02:00
|
|
|
.calledWith(assetStub.livePhotoMotionAsset.id)
|
|
|
|
.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
2022-08-27 07:53:37 +02:00
|
|
|
});
|
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
describe('uploadFile', () => {
|
|
|
|
it('should handle a file upload', async () => {
|
|
|
|
const assetEntity = _getAsset_1();
|
|
|
|
const file = {
|
2024-01-04 21:45:16 +01:00
|
|
|
uuid: 'random-uuid',
|
2023-01-30 17:14:13 +01:00
|
|
|
originalPath: 'fake_path/asset_1.jpeg',
|
|
|
|
mimeType: 'image/jpeg',
|
|
|
|
checksum: Buffer.from('file hash', 'utf8'),
|
|
|
|
originalName: 'asset_1.jpeg',
|
2024-01-13 01:43:36 +01:00
|
|
|
size: 42,
|
2023-01-30 17:14:13 +01:00
|
|
|
};
|
|
|
|
const dto = _getCreateAssetDto();
|
|
|
|
|
2024-02-08 22:56:06 +01:00
|
|
|
assetMock.create.mockResolvedValue(assetEntity);
|
chore(server): Check more permissions in bulk (#5315)
Modify Access repository, to evaluate `authDevice`, `library`, `partner`,
`person`, and `timeline` permissions in bulk.
Queries have been validated to match what they currently generate for
single ids.
As an extra performance improvement, we now use a custom QueryBuilder
for the Partners queries, to avoid the eager relationships that add
unneeded `LEFT JOIN` clauses. We only filter based on the ids present in
the `partners` table, so those joins can be avoided.
Queries:
* `library` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" = $1
AND "LibraryEntity"."ownerId" = $2
AND "LibraryEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
SELECT "LibraryEntity"."id" AS "LibraryEntity_id"
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" IN ($1, $2)
AND "LibraryEntity"."ownerId" = $3
AND "LibraryEntity"."deletedAt" IS NULL
```
* `library` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `authDevice` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" = $2
)
LIMIT 1
-- After
SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id"
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" IN ($2, $3)
```
* `timeline` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `person` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" = $1
AND "PersonEntity"."ownerId" = $2
)
LIMIT 1
-- After
SELECT "PersonEntity"."id" AS "PersonEntity_id"
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" IN ($1, $2)
AND "PersonEntity"."ownerId" = $3
```
* `partner` update access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
2023-11-26 13:50:41 +01:00
|
|
|
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
2023-01-30 17:14:13 +01:00
|
|
|
|
|
|
|
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
|
2023-02-25 15:12:03 +01:00
|
|
|
|
2024-02-08 22:56:06 +01:00
|
|
|
expect(assetMock.create).toHaveBeenCalled();
|
2024-01-13 01:43:36 +01:00
|
|
|
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
2024-02-12 05:40:34 +01:00
|
|
|
expect(storageMock.utimes).toHaveBeenCalledWith(
|
|
|
|
file.originalPath,
|
|
|
|
expect.any(Date),
|
|
|
|
new Date(dto.fileModifiedAt),
|
|
|
|
);
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-01-30 17:14:13 +01:00
|
|
|
it('should handle a duplicate', async () => {
|
|
|
|
const file = {
|
2024-01-04 21:45:16 +01:00
|
|
|
uuid: 'random-uuid',
|
2023-01-30 17:14:13 +01:00
|
|
|
originalPath: 'fake_path/asset_1.jpeg',
|
|
|
|
mimeType: 'image/jpeg',
|
|
|
|
checksum: Buffer.from('file hash', 'utf8'),
|
|
|
|
originalName: 'asset_1.jpeg',
|
2024-01-13 01:43:36 +01:00
|
|
|
size: 0,
|
2023-01-30 17:14:13 +01:00
|
|
|
};
|
|
|
|
const dto = _getCreateAssetDto();
|
2024-01-17 19:24:51 +01:00
|
|
|
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
2023-09-20 13:16:33 +02:00
|
|
|
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
2023-01-30 17:14:13 +01:00
|
|
|
|
2024-02-08 22:56:06 +01:00
|
|
|
assetMock.create.mockRejectedValue(error);
|
2024-01-25 18:52:21 +01:00
|
|
|
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
chore(server): Check more permissions in bulk (#5315)
Modify Access repository, to evaluate `authDevice`, `library`, `partner`,
`person`, and `timeline` permissions in bulk.
Queries have been validated to match what they currently generate for
single ids.
As an extra performance improvement, we now use a custom QueryBuilder
for the Partners queries, to avoid the eager relationships that add
unneeded `LEFT JOIN` clauses. We only filter based on the ids present in
the `partners` table, so those joins can be avoided.
Queries:
* `library` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" = $1
AND "LibraryEntity"."ownerId" = $2
AND "LibraryEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
SELECT "LibraryEntity"."id" AS "LibraryEntity_id"
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" IN ($1, $2)
AND "LibraryEntity"."ownerId" = $3
AND "LibraryEntity"."deletedAt" IS NULL
```
* `library` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `authDevice` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" = $2
)
LIMIT 1
-- After
SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id"
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" IN ($2, $3)
```
* `timeline` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `person` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" = $1
AND "PersonEntity"."ownerId" = $2
)
LIMIT 1
-- After
SELECT "PersonEntity"."id" AS "PersonEntity_id"
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" IN ($1, $2)
AND "PersonEntity"."ownerId" = $3
```
* `partner` update access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
2023-11-26 13:50:41 +01:00
|
|
|
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
2023-01-30 17:14:13 +01:00
|
|
|
|
|
|
|
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue).toHaveBeenCalledWith({
|
|
|
|
name: JobName.DELETE_FILES,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
2024-01-13 01:43:36 +01:00
|
|
|
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should handle a live photo', async () => {
|
|
|
|
const dto = _getCreateAssetDto();
|
2024-01-17 19:24:51 +01:00
|
|
|
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
2023-09-20 13:16:33 +02:00
|
|
|
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
2023-01-30 17:14:13 +01:00
|
|
|
|
2024-02-08 22:56:06 +01:00
|
|
|
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
|
|
|
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
chore(server): Check more permissions in bulk (#5315)
Modify Access repository, to evaluate `authDevice`, `library`, `partner`,
`person`, and `timeline` permissions in bulk.
Queries have been validated to match what they currently generate for
single ids.
As an extra performance improvement, we now use a custom QueryBuilder
for the Partners queries, to avoid the eager relationships that add
unneeded `LEFT JOIN` clauses. We only filter based on the ids present in
the `partners` table, so those joins can be avoided.
Queries:
* `library` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" = $1
AND "LibraryEntity"."ownerId" = $2
AND "LibraryEntity"."deletedAt" IS NULL
)
LIMIT 1
-- After
SELECT "LibraryEntity"."id" AS "LibraryEntity_id"
FROM "libraries" "LibraryEntity"
WHERE
"LibraryEntity"."id" IN ($1, $2)
AND "LibraryEntity"."ownerId" = $3
AND "LibraryEntity"."deletedAt" IS NULL
```
* `library` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `authDevice` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" = $2
)
LIMIT 1
-- After
SELECT "UserTokenEntity"."id" AS "UserTokenEntity_id"
FROM "user_token" "UserTokenEntity"
WHERE
"UserTokenEntity"."userId" = $1
AND "UserTokenEntity"."id" IN ($2, $3)
```
* `timeline` partner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
* `person` owner access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" = $1
AND "PersonEntity"."ownerId" = $2
)
LIMIT 1
-- After
SELECT "PersonEntity"."id" AS "PersonEntity_id"
FROM "person" "PersonEntity"
WHERE
"PersonEntity"."id" IN ($1, $2)
AND "PersonEntity"."ownerId" = $3
```
* `partner` update access:
```sql
-- Before
SELECT 1 AS "row_exists" FROM (SELECT 1 AS dummy_column) "dummy_table" WHERE EXISTS (
SELECT 1
FROM "partners" "PartnerEntity"
LEFT JOIN "users" "PartnerEntity__sharedBy"
ON "PartnerEntity__sharedBy"."id"="PartnerEntity"."sharedById"
AND "PartnerEntity__sharedBy"."deletedAt" IS NULL
LEFT JOIN "users" "PartnerEntity__sharedWith"
ON "PartnerEntity__sharedWith"."id"="PartnerEntity"."sharedWithId"
AND "PartnerEntity__sharedWith"."deletedAt" IS NULL
WHERE
"PartnerEntity"."sharedWithId" = $1
AND "PartnerEntity"."sharedById" = $2
)
LIMIT 1
-- After
SELECT
"partner"."sharedById" AS "partner_sharedById",
"partner"."sharedWithId" AS "partner_sharedWithId"
FROM "partners" "partner"
WHERE
"partner"."sharedById" IN ($1, $2)
AND "partner"."sharedWithId" = $3
```
2023-11-26 13:50:41 +01:00
|
|
|
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
2023-01-30 17:14:13 +01:00
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
await expect(
|
|
|
|
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
|
|
|
|
).resolves.toEqual({
|
2023-01-30 17:14:13 +01:00
|
|
|
duplicate: false,
|
2023-02-25 15:12:03 +01:00
|
|
|
id: 'live-photo-still-asset',
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
expect(jobMock.queue.mock.calls).toEqual([
|
2023-05-27 23:49:57 +02:00
|
|
|
[
|
|
|
|
{
|
|
|
|
name: JobName.METADATA_EXTRACTION,
|
2023-08-01 03:28:07 +02:00
|
|
|
data: { id: assetStub.livePhotoMotionAsset.id, source: 'upload' },
|
2023-05-27 23:49:57 +02:00
|
|
|
},
|
|
|
|
],
|
2023-08-01 03:28:07 +02:00
|
|
|
[{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }],
|
2023-01-30 17:14:13 +01:00
|
|
|
]);
|
2024-01-13 01:43:36 +01:00
|
|
|
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111);
|
2024-02-12 05:40:34 +01:00
|
|
|
expect(storageMock.utimes).toHaveBeenCalledWith(
|
|
|
|
fileStub.livePhotoStill.originalPath,
|
|
|
|
expect.any(Date),
|
|
|
|
new Date(dto.fileModifiedAt),
|
|
|
|
);
|
|
|
|
expect(storageMock.utimes).toHaveBeenCalledWith(
|
|
|
|
fileStub.livePhotoMotion.originalPath,
|
|
|
|
expect.any(Date),
|
|
|
|
new Date(dto.fileModifiedAt),
|
|
|
|
);
|
2023-01-30 17:14:13 +01:00
|
|
|
});
|
2022-09-16 23:47:45 +02:00
|
|
|
});
|
2022-08-27 07:53:37 +02:00
|
|
|
|
2023-05-28 03:56:17 +02:00
|
|
|
describe('bulkUploadCheck', () => {
|
|
|
|
it('should accept hex and base64 checksums', async () => {
|
|
|
|
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
|
|
|
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
|
|
|
|
2024-01-25 18:52:21 +01:00
|
|
|
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([
|
2023-05-28 03:56:17 +02:00
|
|
|
{ id: 'asset-1', checksum: file1 },
|
|
|
|
{ id: 'asset-2', checksum: file2 },
|
|
|
|
]);
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
sut.bulkUploadCheck(authStub.admin, {
|
|
|
|
assets: [
|
|
|
|
{ id: '1', checksum: file1.toString('hex') },
|
|
|
|
{ id: '2', checksum: file2.toString('base64') },
|
|
|
|
],
|
|
|
|
}),
|
|
|
|
).resolves.toEqual({
|
|
|
|
results: [
|
|
|
|
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
|
|
|
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
2024-01-25 18:52:21 +01:00
|
|
|
expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
2023-06-16 21:01:34 +02:00
|
|
|
});
|
|
|
|
});
|
2022-08-27 07:53:37 +02:00
|
|
|
});
|