mirror of
https://github.com/immich-app/immich.git
synced 2025-12-21 01:11:16 +03:00
feat: shared links custom URL (#19999)
* feat: custom url for shared links * feat: use a separate route and query param --------- Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
@@ -13,6 +13,7 @@ import { downloadRequest, withError } from '$lib/utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { asQueryString } from '$lib/utils/shared-links';
|
||||
import {
|
||||
addAssetsToAlbum as addAssets,
|
||||
AssetVisibility,
|
||||
@@ -42,11 +43,11 @@ import { handleError } from './handle-error';
|
||||
|
||||
export const addAssetsToAlbum = async (albumId: string, assetIds: string[], showNotification = true) => {
|
||||
const result = await addAssets({
|
||||
...authManager.params,
|
||||
id: albumId,
|
||||
bulkIdsDto: {
|
||||
ids: assetIds,
|
||||
},
|
||||
key: authManager.key,
|
||||
});
|
||||
const count = result.filter(({ success }) => success).length;
|
||||
const duplicateErrorCount = result.filter(({ error }) => error === 'duplicate').length;
|
||||
@@ -155,7 +156,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
||||
const $preferences = get<UserPreferencesResponseDto | undefined>(preferences);
|
||||
const dto = { ...options, archiveSize: $preferences?.download.archiveSize };
|
||||
|
||||
const [error, downloadInfo] = await withError(() => getDownloadInfo({ downloadInfoDto: dto, key: authManager.key }));
|
||||
const [error, downloadInfo] = await withError(() => getDownloadInfo({ ...authManager.params, downloadInfoDto: dto }));
|
||||
if (error) {
|
||||
const $t = get(t);
|
||||
handleError(error, $t('errors.unable_to_download_files'));
|
||||
@@ -170,7 +171,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
||||
const archive = downloadInfo.archives[index];
|
||||
const suffix = downloadInfo.archives.length > 1 ? `+${index + 1}` : '';
|
||||
const archiveName = fileName.replace('.zip', `${suffix}-${DateTime.now().toFormat('yyyyLLdd_HHmmss')}.zip`);
|
||||
const key = authManager.key;
|
||||
const queryParams = asQueryString(authManager.params);
|
||||
|
||||
let downloadKey = `${archiveName} `;
|
||||
if (downloadInfo.archives.length > 1) {
|
||||
@@ -184,7 +185,7 @@ export const downloadArchive = async (fileName: string, options: Omit<DownloadIn
|
||||
// TODO use sdk once it supports progress events
|
||||
const { data } = await downloadRequest({
|
||||
method: 'POST',
|
||||
url: getBaseUrl() + '/download/archive' + (key ? `?key=${key}` : ''),
|
||||
url: getBaseUrl() + '/download/archive' + (queryParams ? `?${queryParams}` : ''),
|
||||
data: { assetIds: archive.assetIds },
|
||||
signal: abort.signal,
|
||||
onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded),
|
||||
@@ -217,7 +218,7 @@ export const downloadFile = async (asset: AssetResponseDto) => {
|
||||
};
|
||||
|
||||
if (asset.livePhotoVideoId) {
|
||||
const motionAsset = await getAssetInfo({ id: asset.livePhotoVideoId, key: authManager.key });
|
||||
const motionAsset = await getAssetInfo({ ...authManager.params, id: asset.livePhotoVideoId });
|
||||
if (!isAndroidMotionVideo(motionAsset) || get(preferences)?.download.includeEmbeddedVideos) {
|
||||
assets.push({
|
||||
filename: motionAsset.originalFileName,
|
||||
@@ -227,16 +228,16 @@ export const downloadFile = async (asset: AssetResponseDto) => {
|
||||
}
|
||||
}
|
||||
|
||||
const queryParams = asQueryString(authManager.params);
|
||||
|
||||
for (const { filename, id } of assets) {
|
||||
try {
|
||||
const key = authManager.key;
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('downloading_asset_filename', { values: { filename: asset.originalFileName } }),
|
||||
});
|
||||
|
||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (key ? `?key=${key}` : ''), filename);
|
||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import { uploadRequest } from '$lib/utils';
|
||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||
import { ExecutorQueue } from '$lib/utils/executor-queue';
|
||||
import { asQueryString } from '$lib/utils/shared-links';
|
||||
import {
|
||||
Action,
|
||||
AssetMediaStatus,
|
||||
@@ -152,8 +153,7 @@ async function fileUploader({
|
||||
}
|
||||
|
||||
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
|
||||
const key = authManager.key;
|
||||
if (crypto?.subtle?.digest && !key) {
|
||||
if (crypto?.subtle?.digest && !authManager.isSharedLink) {
|
||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
||||
await tick();
|
||||
try {
|
||||
@@ -179,10 +179,12 @@ async function fileUploader({
|
||||
}
|
||||
|
||||
if (!responseData) {
|
||||
const queryParams = asQueryString(authManager.params);
|
||||
|
||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_uploading') });
|
||||
if (replaceAssetId) {
|
||||
const response = await uploadRequest<AssetMediaResponseDto>({
|
||||
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (key ? `?key=${key}` : ''),
|
||||
url: getBaseUrl() + getAssetOriginalPath(replaceAssetId) + (queryParams ? `?${queryParams}` : ''),
|
||||
method: 'PUT',
|
||||
data: formData,
|
||||
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
|
||||
@@ -190,7 +192,7 @@ async function fileUploader({
|
||||
responseData = response.data;
|
||||
} else {
|
||||
const response = await uploadRequest<AssetMediaResponseDto>({
|
||||
url: getBaseUrl() + '/assets' + (key ? `?key=${key}` : ''),
|
||||
url: getBaseUrl() + '/assets' + (queryParams ? `?${queryParams}` : ''),
|
||||
data: formData,
|
||||
onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
|
||||
});
|
||||
|
||||
@@ -13,7 +13,8 @@ export const isExternalUrl = (url: string): boolean => {
|
||||
};
|
||||
|
||||
export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');
|
||||
export const isSharedLinkRoute = (route?: string | null) => !!route?.startsWith('/(user)/share/[key]');
|
||||
export const isSharedLinkRoute = (route?: string | null) =>
|
||||
!!route?.startsWith('/(user)/share/[key]') || !!route?.startsWith('/(user)/s/[slug]');
|
||||
export const isSearchRoute = (route?: string | null) => !!route?.startsWith('/(user)/search');
|
||||
export const isAlbumsRoute = (route?: string | null) => !!route?.startsWith('/(user)/albums/[albumId=id]');
|
||||
export const isPeopleRoute = (route?: string | null) => !!route?.startsWith('/(user)/people/[personId]');
|
||||
|
||||
58
web/src/lib/utils/shared-links.ts
Normal file
58
web/src/lib/utils/shared-links.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
||||
import { getMySharedLink, isHttpError } from '@immich/sdk';
|
||||
|
||||
export const asQueryString = ({ slug, key }: { slug?: string; key?: string }) => {
|
||||
const params = new URLSearchParams();
|
||||
if (slug) {
|
||||
params.set('slug', slug);
|
||||
}
|
||||
|
||||
if (key) {
|
||||
params.set('key', key);
|
||||
}
|
||||
|
||||
return params.toString();
|
||||
};
|
||||
|
||||
export const loadSharedLink = async ({ url, params }: { url: URL; params: { key?: string; slug?: string } }) => {
|
||||
const { key, slug } = params;
|
||||
await authenticate(url, { public: true });
|
||||
|
||||
const common = { key, slug };
|
||||
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const [sharedLink, asset] = await Promise.all([getMySharedLink({ key, slug }), getAssetInfoFromParam(params)]);
|
||||
setSharedLink(sharedLink);
|
||||
const assetCount = sharedLink.assets.length;
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
const assetPath = assetId ? getAssetThumbnailUrl(assetId) : '/feature-panel.png';
|
||||
|
||||
return {
|
||||
...common,
|
||||
sharedLink,
|
||||
asset,
|
||||
meta: {
|
||||
title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'),
|
||||
description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }),
|
||||
imageUrl: assetPath,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isHttpError(error) && error.data.message === 'Invalid password') {
|
||||
return {
|
||||
...common,
|
||||
passwordRequired: true,
|
||||
meta: {
|
||||
title: $t('password_required'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user