1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

refactor(server): download file (#1512)

* refactor(server): download file

* chore: generate open-api and remove unused refs

* chore(server): tests

* chore: remove unused code
This commit is contained in:
Jason Rasmussen 2023-02-03 10:16:25 -05:00 committed by GitHub
parent e39507552f
commit 2b0b2bb1ae
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 210 additions and 291 deletions

View file

@ -26,14 +26,10 @@ class ImageViewerService {
if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) { if (asset.type == AssetTypeEnum.IMAGE && asset.livePhotoVideoId != null) {
var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo( var imageResponse = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id, asset.id,
isThumb: false,
isWeb: false,
); );
var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo( var motionReponse = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.livePhotoVideoId!, asset.livePhotoVideoId!,
isThumb: false,
isWeb: false,
); );
final AssetEntity? entity; final AssetEntity? entity;
@ -54,8 +50,6 @@ class ImageViewerService {
} else { } else {
var res = await _apiService.assetApi.downloadFileWithHttpInfo( var res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.id, asset.id,
isThumb: false,
isWeb: false,
); );
final AssetEntity? entity; final AssetEntity? entity;

View file

@ -29,8 +29,6 @@ class ShareService {
final tempFile = await File('${tempDir.path}/$fileName').create(); final tempFile = await File('${tempDir.path}/$fileName').create();
final res = await _apiService.assetApi.downloadFileWithHttpInfo( final res = await _apiService.assetApi.downloadFileWithHttpInfo(
asset.remote!.id, asset.remote!.id,
isThumb: false,
isWeb: false,
); );
tempFile.writeAsBytesSync(res.bodyBytes); tempFile.writeAsBytesSync(res.bodyBytes);
return XFile(tempFile.path); return XFile(tempFile.path);

View file

@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.43.0 - API version: 1.43.1
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements ## Requirements

View file

@ -230,7 +230,7 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **downloadFile** # **downloadFile**
> Object downloadFile(assetId, isThumb, isWeb) > Object downloadFile(assetId)
@ -248,11 +248,9 @@ import 'package:openapi/api.dart';
final api_instance = AssetApi(); final api_instance = AssetApi();
final assetId = assetId_example; // String | final assetId = assetId_example; // String |
final isThumb = true; // bool |
final isWeb = true; // bool |
try { try {
final result = api_instance.downloadFile(assetId, isThumb, isWeb); final result = api_instance.downloadFile(assetId);
print(result); print(result);
} catch (e) { } catch (e) {
print('Exception when calling AssetApi->downloadFile: $e\n'); print('Exception when calling AssetApi->downloadFile: $e\n');
@ -264,8 +262,6 @@ try {
Name | Type | Description | Notes Name | Type | Description | Notes
------------- | ------------- | ------------- | ------------- ------------- | ------------- | ------------- | -------------
**assetId** | **String**| | **assetId** | **String**| |
**isThumb** | **bool**| | [optional]
**isWeb** | **bool**| | [optional]
### Return type ### Return type

View file

@ -234,11 +234,7 @@ class AssetApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] assetId (required): /// * [String] assetId (required):
/// Future<Response> downloadFileWithHttpInfo(String assetId,) async {
/// * [bool] isThumb:
///
/// * [bool] isWeb:
Future<Response> downloadFileWithHttpInfo(String assetId, { bool? isThumb, bool? isWeb, }) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/asset/download/{assetId}' final path = r'/asset/download/{assetId}'
.replaceAll('{assetId}', assetId); .replaceAll('{assetId}', assetId);
@ -250,13 +246,6 @@ class AssetApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <String, String>{}; final formParams = <String, String>{};
if (isThumb != null) {
queryParams.addAll(_queryParams('', 'isThumb', isThumb));
}
if (isWeb != null) {
queryParams.addAll(_queryParams('', 'isWeb', isWeb));
}
const contentTypes = <String>[]; const contentTypes = <String>[];
@ -276,12 +265,8 @@ class AssetApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] assetId (required): /// * [String] assetId (required):
/// Future<Object?> downloadFile(String assetId,) async {
/// * [bool] isThumb: final response = await downloadFileWithHttpInfo(assetId,);
///
/// * [bool] isWeb:
Future<Object?> downloadFile(String assetId, { bool? isThumb, bool? isWeb, }) async {
final response = await downloadFileWithHttpInfo(assetId, isThumb: isThumb, isWeb: isWeb, );
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View file

@ -43,51 +43,48 @@ class AlbumResponseDto {
List<AssetResponseDto> assets; List<AssetResponseDto> assets;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => identical(this, other) || other is AlbumResponseDto &&
identical(this, other) || other.assetCount == assetCount &&
other is AlbumResponseDto && other.id == id &&
other.assetCount == assetCount && other.ownerId == ownerId &&
other.id == id && other.albumName == albumName &&
other.ownerId == ownerId && other.createdAt == createdAt &&
other.albumName == albumName && other.albumThumbnailAssetId == albumThumbnailAssetId &&
other.createdAt == createdAt && other.shared == shared &&
other.albumThumbnailAssetId == albumThumbnailAssetId && other.sharedUsers == sharedUsers &&
other.shared == shared && other.assets == assets;
other.sharedUsers == sharedUsers &&
other.assets == assets;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(assetCount.hashCode) + (assetCount.hashCode) +
(id.hashCode) + (id.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(albumName.hashCode) + (albumName.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) + (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode) +
(shared.hashCode) + (shared.hashCode) +
(sharedUsers.hashCode) + (sharedUsers.hashCode) +
(assets.hashCode); (assets.hashCode);
@override @override
String toString() => String toString() => 'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
'AlbumResponseDto[assetCount=$assetCount, id=$id, ownerId=$ownerId, albumName=$albumName, createdAt=$createdAt, albumThumbnailAssetId=$albumThumbnailAssetId, shared=$shared, sharedUsers=$sharedUsers, assets=$assets]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'assetCount'] = this.assetCount; json[r'assetCount'] = this.assetCount;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
json[r'albumName'] = this.albumName; json[r'albumName'] = this.albumName;
json[r'createdAt'] = this.createdAt; json[r'createdAt'] = this.createdAt;
if (this.albumThumbnailAssetId != null) { if (this.albumThumbnailAssetId != null) {
json[r'albumThumbnailAssetId'] = this.albumThumbnailAssetId; json[r'albumThumbnailAssetId'] = this.albumThumbnailAssetId;
} else { } else {
// json[r'albumThumbnailAssetId'] = null; // json[r'albumThumbnailAssetId'] = null;
} }
json[r'shared'] = this.shared; json[r'shared'] = this.shared;
json[r'sharedUsers'] = this.sharedUsers; json[r'sharedUsers'] = this.sharedUsers;
json[r'assets'] = this.assets; json[r'assets'] = this.assets;
return json; return json;
} }
@ -101,13 +98,13 @@ class AlbumResponseDto {
// Ensure that the map contains the required keys. // Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null. // Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode! // Note 2: this code is stripped in release mode!
// assert(() { assert(() {
// requiredKeys.forEach((key) { requiredKeys.forEach((key) {
// assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.'); assert(json.containsKey(key), 'Required key "AlbumResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.'); assert(json[key] != null, 'Required key "AlbumResponseDto[$key]" has a null value in JSON.');
// }); });
// return true; return true;
// }()); }());
return AlbumResponseDto( return AlbumResponseDto(
assetCount: mapValueOfType<int>(json, r'assetCount')!, assetCount: mapValueOfType<int>(json, r'assetCount')!,
@ -115,8 +112,7 @@ class AlbumResponseDto {
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
albumName: mapValueOfType<String>(json, r'albumName')!, albumName: mapValueOfType<String>(json, r'albumName')!,
createdAt: mapValueOfType<String>(json, r'createdAt')!, createdAt: mapValueOfType<String>(json, r'createdAt')!,
albumThumbnailAssetId: albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
mapValueOfType<String>(json, r'albumThumbnailAssetId'),
shared: mapValueOfType<bool>(json, r'shared')!, shared: mapValueOfType<bool>(json, r'shared')!,
sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!, sharedUsers: UserResponseDto.listFromJson(json[r'sharedUsers'])!,
assets: AssetResponseDto.listFromJson(json[r'assets'])!, assets: AssetResponseDto.listFromJson(json[r'assets'])!,
@ -125,10 +121,7 @@ class AlbumResponseDto {
return null; return null;
} }
static List<AlbumResponseDto>? listFromJson( static List<AlbumResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final result = <AlbumResponseDto>[]; final result = <AlbumResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
@ -156,18 +149,12 @@ class AlbumResponseDto {
} }
// maps a json object with a list of AlbumResponseDto-objects as value to a dart map // maps a json object with a list of AlbumResponseDto-objects as value to a dart map
static Map<String, List<AlbumResponseDto>> mapListFromJson( static Map<String, List<AlbumResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AlbumResponseDto>>{}; final map = <String, List<AlbumResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AlbumResponseDto.listFromJson( final value = AlbumResponseDto.listFromJson(entry.value, growable: growable,);
entry.value,
growable: growable,
);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@ -189,3 +176,4 @@ class AlbumResponseDto {
'assets', 'assets',
}; };
} }

View file

@ -82,76 +82,73 @@ class AssetResponseDto {
List<TagResponseDto> tags; List<TagResponseDto> tags;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto &&
identical(this, other) || other.type == type &&
other is AssetResponseDto && other.id == id &&
other.type == type && other.deviceAssetId == deviceAssetId &&
other.id == id && other.ownerId == ownerId &&
other.deviceAssetId == deviceAssetId && other.deviceId == deviceId &&
other.ownerId == ownerId && other.originalPath == originalPath &&
other.deviceId == deviceId && other.resizePath == resizePath &&
other.originalPath == originalPath && other.createdAt == createdAt &&
other.resizePath == resizePath && other.modifiedAt == modifiedAt &&
other.createdAt == createdAt && other.isFavorite == isFavorite &&
other.modifiedAt == modifiedAt && other.mimeType == mimeType &&
other.isFavorite == isFavorite && other.duration == duration &&
other.mimeType == mimeType && other.webpPath == webpPath &&
other.duration == duration && other.encodedVideoPath == encodedVideoPath &&
other.webpPath == webpPath && other.exifInfo == exifInfo &&
other.encodedVideoPath == encodedVideoPath && other.smartInfo == smartInfo &&
other.exifInfo == exifInfo && other.livePhotoVideoId == livePhotoVideoId &&
other.smartInfo == smartInfo && other.tags == tags;
other.livePhotoVideoId == livePhotoVideoId &&
other.tags == tags;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(type.hashCode) + (type.hashCode) +
(id.hashCode) + (id.hashCode) +
(deviceAssetId.hashCode) + (deviceAssetId.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(deviceId.hashCode) + (deviceId.hashCode) +
(originalPath.hashCode) + (originalPath.hashCode) +
(resizePath == null ? 0 : resizePath!.hashCode) + (resizePath == null ? 0 : resizePath!.hashCode) +
(createdAt.hashCode) + (createdAt.hashCode) +
(modifiedAt.hashCode) + (modifiedAt.hashCode) +
(isFavorite.hashCode) + (isFavorite.hashCode) +
(mimeType == null ? 0 : mimeType!.hashCode) + (mimeType == null ? 0 : mimeType!.hashCode) +
(duration.hashCode) + (duration.hashCode) +
(webpPath == null ? 0 : webpPath!.hashCode) + (webpPath == null ? 0 : webpPath!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(exifInfo == null ? 0 : exifInfo!.hashCode) + (exifInfo == null ? 0 : exifInfo!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) + (smartInfo == null ? 0 : smartInfo!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(tags.hashCode); (tags.hashCode);
@override @override
String toString() => String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, resizePath=$resizePath, createdAt=$createdAt, modifiedAt=$modifiedAt, isFavorite=$isFavorite, mimeType=$mimeType, duration=$duration, webpPath=$webpPath, encodedVideoPath=$encodedVideoPath, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'type'] = this.type; json[r'type'] = this.type;
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'deviceAssetId'] = this.deviceAssetId; json[r'deviceAssetId'] = this.deviceAssetId;
json[r'ownerId'] = this.ownerId; json[r'ownerId'] = this.ownerId;
json[r'deviceId'] = this.deviceId; json[r'deviceId'] = this.deviceId;
json[r'originalPath'] = this.originalPath; json[r'originalPath'] = this.originalPath;
if (this.resizePath != null) { if (this.resizePath != null) {
json[r'resizePath'] = this.resizePath; json[r'resizePath'] = this.resizePath;
} else { } else {
// json[r'resizePath'] = null; // json[r'resizePath'] = null;
} }
json[r'createdAt'] = this.createdAt; json[r'createdAt'] = this.createdAt;
json[r'modifiedAt'] = this.modifiedAt; json[r'modifiedAt'] = this.modifiedAt;
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
if (this.mimeType != null) { if (this.mimeType != null) {
json[r'mimeType'] = this.mimeType; json[r'mimeType'] = this.mimeType;
} else { } else {
// json[r'mimeType'] = null; // json[r'mimeType'] = null;
} }
json[r'duration'] = this.duration; json[r'duration'] = this.duration;
if (this.webpPath != null) { if (this.webpPath != null) {
json[r'webpPath'] = this.webpPath; json[r'webpPath'] = this.webpPath;
} else { } else {
@ -177,7 +174,7 @@ class AssetResponseDto {
} else { } else {
// json[r'livePhotoVideoId'] = null; // json[r'livePhotoVideoId'] = null;
} }
json[r'tags'] = this.tags; json[r'tags'] = this.tags;
return json; return json;
} }
@ -191,13 +188,13 @@ class AssetResponseDto {
// Ensure that the map contains the required keys. // Ensure that the map contains the required keys.
// Note 1: the values aren't checked for validity beyond being non-null. // Note 1: the values aren't checked for validity beyond being non-null.
// Note 2: this code is stripped in release mode! // Note 2: this code is stripped in release mode!
// assert(() { assert(() {
// requiredKeys.forEach((key) { requiredKeys.forEach((key) {
// assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.'); assert(json.containsKey(key), 'Required key "AssetResponseDto[$key]" is missing from JSON.');
// assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.'); assert(json[key] != null, 'Required key "AssetResponseDto[$key]" has a null value in JSON.');
// }); });
// return true; return true;
// }()); }());
return AssetResponseDto( return AssetResponseDto(
type: AssetTypeEnum.fromJson(json[r'type'])!, type: AssetTypeEnum.fromJson(json[r'type'])!,
@ -223,10 +220,7 @@ class AssetResponseDto {
return null; return null;
} }
static List<AssetResponseDto>? listFromJson( static List<AssetResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final result = <AssetResponseDto>[]; final result = <AssetResponseDto>[];
if (json is List && json.isNotEmpty) { if (json is List && json.isNotEmpty) {
for (final row in json) { for (final row in json) {
@ -254,18 +248,12 @@ class AssetResponseDto {
} }
// maps a json object with a list of AssetResponseDto-objects as value to a dart map // maps a json object with a list of AssetResponseDto-objects as value to a dart map
static Map<String, List<AssetResponseDto>> mapListFromJson( static Map<String, List<AssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
dynamic json, {
bool growable = false,
}) {
final map = <String, List<AssetResponseDto>>{}; final map = <String, List<AssetResponseDto>>{};
if (json is Map && json.isNotEmpty) { if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) { for (final entry in json.entries) {
final value = AssetResponseDto.listFromJson( final value = AssetResponseDto.listFromJson(entry.value, growable: growable,);
entry.value,
growable: growable,
);
if (value != null) { if (value != null) {
map[entry.key] = value; map[entry.key] = value;
} }
@ -292,3 +280,4 @@ class AssetResponseDto {
'tags', 'tags',
}; };
} }

View file

@ -47,7 +47,7 @@ void main() {
// //
// //
//Future<Object> downloadFile(String assetId, { bool isThumb, bool isWeb }) async //Future<Object> downloadFile(String assetId) async
test('test downloadFile', () async { test('test downloadFile', () async {
// TODO // TODO
}); });

View file

@ -15,6 +15,7 @@ import {
Put, Put,
UploadedFiles, UploadedFiles,
Patch, Patch,
StreamableFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator'; import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
@ -28,7 +29,7 @@ import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger'; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
import { AssetResponseDto } from '@app/domain'; import { AssetResponseDto, ImmichReadStream } from '@app/domain';
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto'; import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
import { AssetFileUploadDto } from './dto/asset-file-upload.dto'; import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto'; import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
@ -55,6 +56,10 @@ import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto
import { AssetSearchDto } from './dto/asset-search.dto'; import { AssetSearchDto } from './dto/asset-search.dto';
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config'; import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
return new StreamableFile(stream, { type, length });
}
@ApiBearerAuth() @ApiBearerAuth()
@ApiTags('Asset') @ApiTags('Asset')
@Controller('asset') @Controller('asset')
@ -103,12 +108,9 @@ export class AssetController {
async downloadFile( async downloadFile(
@GetAuthUser() authUser: AuthUserDto, @GetAuthUser() authUser: AuthUserDto,
@Response({ passthrough: true }) res: Res, @Response({ passthrough: true }) res: Res,
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string, @Param('assetId') assetId: string,
): Promise<any> { ): Promise<any> {
this.assetService.checkDownloadAccess(authUser); return this.assetService.downloadFile(authUser, assetId).then(asStreamableFile);
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.downloadFile(query, assetId, res);
} }
@Authenticated({ isShared: true }) @Authenticated({ isShared: true })

View file

@ -9,12 +9,13 @@ import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-use
import { DownloadService } from '../../modules/download/download.service'; import { DownloadService } from '../../modules/download/download.service';
import { AlbumRepository, IAlbumRepository } from '../album/album-repository'; import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
import { StorageService } from '@app/storage'; import { StorageService } from '@app/storage';
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, JobName } from '@app/domain'; import { ICryptoRepository, IJobRepository, ISharedLinkRepository, IStorageRepository, JobName } from '@app/domain';
import { import {
authStub, authStub,
newCryptoRepositoryMock, newCryptoRepositoryMock,
newJobRepositoryMock, newJobRepositoryMock,
newSharedLinkRepositoryMock, newSharedLinkRepositoryMock,
newStorageRepositoryMock,
sharedLinkResponseStub, sharedLinkResponseStub,
sharedLinkStub, sharedLinkStub,
} from '@app/domain/../test'; } from '@app/domain/../test';
@ -110,6 +111,7 @@ describe('AssetService', () => {
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>; let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
let cryptoMock: jest.Mocked<ICryptoRepository>; let cryptoMock: jest.Mocked<ICryptoRepository>;
let jobMock: jest.Mocked<IJobRepository>; let jobMock: jest.Mocked<IJobRepository>;
let storageMock: jest.Mocked<IStorageRepository>;
beforeEach(() => { beforeEach(() => {
assetRepositoryMock = { assetRepositoryMock = {
@ -154,6 +156,7 @@ describe('AssetService', () => {
sharedLinkRepositoryMock = newSharedLinkRepositoryMock(); sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
jobMock = newJobRepositoryMock(); jobMock = newJobRepositoryMock();
cryptoMock = newCryptoRepositoryMock(); cryptoMock = newCryptoRepositoryMock();
storageMock = newStorageRepositoryMock();
sut = new AssetService( sut = new AssetService(
assetRepositoryMock, assetRepositoryMock,
@ -164,6 +167,7 @@ describe('AssetService', () => {
sharedLinkRepositoryMock, sharedLinkRepositoryMock,
jobMock, jobMock,
cryptoMock, cryptoMock,
storageMock,
); );
}); });
@ -413,4 +417,15 @@ describe('AssetService', () => {
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException); expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
}); });
}); });
describe('downloadFile', () => {
it('should download a single file', async () => {
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
assetRepositoryMock.get.mockResolvedValue(_getAsset_1());
await sut.downloadFile(authStub.admin, 'id_1');
expect(storageMock.createReadStream).toHaveBeenCalledWith('fake_path/asset_1.jpeg', 'image/jpeg');
});
});
}); });

View file

@ -10,7 +10,6 @@ import {
StreamableFile, StreamableFile,
} from '@nestjs/common'; } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import { createHash } from 'node:crypto';
import { QueryFailedError, Repository } from 'typeorm'; import { QueryFailedError, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType, SharedLinkType } from '@app/infra'; import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
@ -23,7 +22,14 @@ import { SearchAssetDto } from './dto/search-asset.dto';
import fs from 'fs/promises'; import fs from 'fs/promises';
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto'; import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { AssetResponseDto, JobName, mapAsset, mapAssetWithoutExif } from '@app/domain'; import {
AssetResponseDto,
ImmichReadStream,
IStorageRepository,
JobName,
mapAsset,
mapAssetWithoutExif,
} from '@app/domain';
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto'; import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto'; import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto'; import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
@ -73,6 +79,7 @@ export class AssetService {
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository, @Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
@Inject(IStorageRepository) private storage: IStorageRepository,
) { ) {
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService); this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository); this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
@ -189,62 +196,21 @@ export class AssetService {
return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload); return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
} }
public async downloadFile(query: ServeFileDto, assetId: string, res: Res) { public async downloadFile(authUser: AuthUserDto, assetId: string): Promise<ImmichReadStream> {
this.checkDownloadAccess(authUser);
await this.checkAssetsAccess(authUser, [assetId]);
try { try {
let fileReadStream = null; const asset = await this._assetRepository.get(assetId);
const asset = await this._assetRepository.getById(assetId); if (asset && asset.originalPath && asset.mimeType) {
return this.storage.createReadStream(asset.originalPath, asset.mimeType);
// Download Video
if (asset.type === AssetType.VIDEO) {
const { size } = await fileInfo(asset.originalPath);
res.set({
'Content-Type': asset.mimeType,
'Content-Length': size,
});
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.originalPath);
} else {
// Download Image
if (!query.isThumb) {
/**
* Download Image Original File
*/
const { size } = await fileInfo(asset.originalPath);
res.set({
'Content-Type': asset.mimeType,
'Content-Length': size,
});
await fs.access(asset.originalPath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.originalPath);
} else {
/**
* Download Image Resize File
*/
if (!asset.resizePath) {
throw new NotFoundException('resizePath not set');
}
const { size } = await fileInfo(asset.resizePath);
res.set({
'Content-Type': 'image/jpeg',
'Content-Length': size,
});
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
fileReadStream = createReadStream(asset.resizePath);
}
} }
return new StreamableFile(fileReadStream);
} catch (e) { } catch (e) {
Logger.error(`Error download asset ${e}`, 'downloadFile'); Logger.error(`Error download asset ${e}`, 'downloadFile');
throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile'); throw new InternalServerErrorException(`Failed to download asset ${e}`, 'DownloadFile');
} }
throw new NotFoundException();
} }
public async getAssetThumbnail( public async getAssetThumbnail(
@ -255,8 +221,7 @@ export class AssetService {
) { ) {
let fileReadStream: ReadStream; let fileReadStream: ReadStream;
const asset = await this.assetRepository.findOne({ where: { id: assetId } }); const asset = await this._assetRepository.get(assetId);
if (!asset) { if (!asset) {
throw new NotFoundException('Asset not found'); throw new NotFoundException('Asset not found');
} }
@ -584,18 +549,6 @@ export class AssetService {
return this._assetRepository.getAssetByChecksum(userId, checksum); return this._assetRepository.getAssetByChecksum(userId, checksum);
} }
calculateChecksum(filePath: string): Promise<Buffer> {
const fileReadStream = createReadStream(filePath);
const sha1Hash = createHash('sha1');
const deferred = new Promise<Buffer>((resolve, reject) => {
sha1Hash.once('error', (err) => reject(err));
sha1Hash.once('finish', () => resolve(sha1Hash.read()));
});
fileReadStream.pipe(sha1Hash);
return deferred;
}
getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> { getAssetCountByUserId(authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
return this._assetRepository.getAssetCountByUserId(authUser.id); return this._assetRepository.getAssetCountByUserId(authUser.id);
} }

View file

@ -1109,24 +1109,6 @@
"operationId": "downloadFile", "operationId": "downloadFile",
"description": "", "description": "",
"parameters": [ "parameters": [
{
"name": "isThumb",
"required": false,
"in": "query",
"schema": {
"title": "Is serve thumbnail (resize) file",
"type": "boolean"
}
},
{
"name": "isWeb",
"required": false,
"in": "query",
"schema": {
"title": "Is request made from web",
"type": "boolean"
}
},
{ {
"name": "assetId", "name": "assetId",
"required": true, "required": true,

View file

@ -6,11 +6,6 @@ import { ICryptoRepository } from '../crypto/crypto.repository';
import { LoginResponseDto, mapLoginResponse } from './response-dto'; import { LoginResponseDto, mapLoginResponse } from './response-dto';
import { IUserTokenRepository, UserTokenCore } from '../user-token'; import { IUserTokenRepository, UserTokenCore } from '../user-token';
export type JwtValidationResult = {
status: boolean;
userId: string | null;
};
export class AuthCore { export class AuthCore {
private userTokenCore: UserTokenCore; private userTokenCore: UserTokenCore;
constructor( constructor(

View file

@ -1,5 +1,4 @@
export * from './auth-user.dto'; export * from './auth-user.dto';
export * from './change-password.dto'; export * from './change-password.dto';
export * from './jwt-payload.dto';
export * from './login-credential.dto'; export * from './login-credential.dto';
export * from './sign-up.dto'; export * from './sign-up.dto';

View file

@ -1,4 +0,0 @@
export class JwtPayloadDto {
userId!: string;
email!: string;
}

View file

@ -8,6 +8,7 @@ export * from './domain.module';
export * from './job'; export * from './job';
export * from './oauth'; export * from './oauth';
export * from './share'; export * from './share';
export * from './storage';
export * from './system-config'; export * from './system-config';
export * from './tag'; export * from './tag';
export * from './user'; export * from './user';

View file

@ -0,0 +1 @@
export * from './storage.repository';

View file

@ -0,0 +1,13 @@
import { ReadStream } from 'fs';
export interface ImmichReadStream {
stream: ReadStream;
type: string;
length: number;
}
export const IStorageRepository = 'IStorageRepository';
export interface IStorageRepository {
createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream>;
}

View file

@ -4,6 +4,7 @@ export * from './device-info.repository.mock';
export * from './fixtures'; export * from './fixtures';
export * from './job.repository.mock'; export * from './job.repository.mock';
export * from './shared-link.repository.mock'; export * from './shared-link.repository.mock';
export * from './storage.repository.mock';
export * from './system-config.repository.mock'; export * from './system-config.repository.mock';
export * from './user-token.repository.mock'; export * from './user-token.repository.mock';
export * from './user.repository.mock'; export * from './user.repository.mock';

View file

@ -0,0 +1,7 @@
import { IStorageRepository } from '../src';
export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
return {
createReadStream: jest.fn(),
};
};

View file

@ -4,6 +4,7 @@ import {
IJobRepository, IJobRepository,
IKeyRepository, IKeyRepository,
ISharedLinkRepository, ISharedLinkRepository,
IStorageRepository,
ISystemConfigRepository, ISystemConfigRepository,
IUserRepository, IUserRepository,
QueueName, QueueName,
@ -29,6 +30,7 @@ import {
UserTokenEntity, UserTokenEntity,
} from './db'; } from './db';
import { JobRepository } from './job'; import { JobRepository } from './job';
import { FilesystemProvider } from './storage';
const providers: Provider[] = [ const providers: Provider[] = [
{ provide: ICryptoRepository, useClass: CryptoRepository }, { provide: ICryptoRepository, useClass: CryptoRepository },
@ -36,6 +38,7 @@ const providers: Provider[] = [
{ provide: IKeyRepository, useClass: APIKeyRepository }, { provide: IKeyRepository, useClass: APIKeyRepository },
{ provide: IJobRepository, useClass: JobRepository }, { provide: IJobRepository, useClass: JobRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository }, { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStorageRepository, useClass: FilesystemProvider },
{ provide: ISystemConfigRepository, useClass: SystemConfigRepository }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
{ provide: IUserRepository, useClass: UserRepository }, { provide: IUserRepository, useClass: UserRepository },
{ provide: IUserTokenRepository, useClass: UserTokenRepository }, { provide: IUserTokenRepository, useClass: UserTokenRepository },

View file

@ -0,0 +1,18 @@
import { ImmichReadStream, IStorageRepository } from '@app/domain';
import { constants, createReadStream, stat } from 'fs';
import fs from 'fs/promises';
import { promisify } from 'util';
const fileInfo = promisify(stat);
export class FilesystemProvider implements IStorageRepository {
async createReadStream(filepath: string, mimeType: string): Promise<ImmichReadStream> {
const { size } = await fileInfo(filepath);
await fs.access(filepath, constants.R_OK | constants.W_OK);
return {
stream: createReadStream(filepath),
length: size,
type: mimeType,
};
}
}

View file

@ -0,0 +1 @@
export * from './filesystem.provider';

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.43.0 * The version of the OpenAPI document: 1.43.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -3729,12 +3729,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
/** /**
* *
* @param {string} assetId * @param {string} assetId
* @param {boolean} [isThumb]
* @param {boolean} [isWeb]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
downloadFile: async (assetId: string, isThumb?: boolean, isWeb?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { downloadFile: async (assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetId' is not null or undefined // verify required parameter 'assetId' is not null or undefined
assertParamExists('downloadFile', 'assetId', assetId) assertParamExists('downloadFile', 'assetId', assetId)
const localVarPath = `/asset/download/{assetId}` const localVarPath = `/asset/download/{assetId}`
@ -3754,14 +3752,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
// http bearer authentication required // http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration) await setBearerAuthToObject(localVarHeaderParameter, configuration)
if (isThumb !== undefined) {
localVarQueryParameter['isThumb'] = isThumb;
}
if (isWeb !== undefined) {
localVarQueryParameter['isWeb'] = isWeb;
}
setSearchParams(localVarUrlObj, localVarQueryParameter); setSearchParams(localVarUrlObj, localVarQueryParameter);
@ -4489,13 +4479,11 @@ export const AssetApiFp = function(configuration?: Configuration) {
/** /**
* *
* @param {string} assetId * @param {string} assetId
* @param {boolean} [isThumb]
* @param {boolean} [isWeb]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> { async downloadFile(assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options); const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/** /**
@ -4719,13 +4707,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
/** /**
* *
* @param {string} assetId * @param {string} assetId
* @param {boolean} [isThumb]
* @param {boolean} [isWeb]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
*/ */
downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> { downloadFile(assetId: string, options?: any): AxiosPromise<object> {
return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath)); return localVarFp.downloadFile(assetId, options).then((request) => request(axios, basePath));
}, },
/** /**
* *
@ -4939,14 +4925,12 @@ export class AssetApi extends BaseAPI {
/** /**
* *
* @param {string} assetId * @param {string} assetId
* @param {boolean} [isThumb]
* @param {boolean} [isWeb]
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @throws {RequiredError} * @throws {RequiredError}
* @memberof AssetApi * @memberof AssetApi
*/ */
public downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: AxiosRequestConfig) { public downloadFile(assetId: string, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).downloadFile(assetId, options).then((request) => request(this.axios, this.basePath));
} }
/** /**

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.43.0 * The version of the OpenAPI document: 1.43.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.43.0 * The version of the OpenAPI document: 1.43.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.43.0 * The version of the OpenAPI document: 1.43.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.43.0 * The version of the OpenAPI document: 1.43.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -136,10 +136,8 @@
$downloadAssets[imageFileName] = 0; $downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile(assetId, false, false, { const { data, status } = await api.assetApi.downloadFile(assetId, {
params: { params: { key },
key
},
responseType: 'blob', responseType: 'blob',
onDownloadProgress: (progressEvent) => { onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) { if (progressEvent.lengthComputable) {