Point hotlink viewer links at the viewer page path.

Hotlink share links were using the zero-knowledge viewer URL. The direct image URL stays separate for embeds.
This commit is contained in:
2026-10-25 23:39:33 +01:00
parent ee9f80da5f
commit 7b70788dcf
4 changed files with 105 additions and 10 deletions
@@ -0,0 +1,8 @@
import { describe, expect, it } from 'vitest';
import { buildHotlinkViewerPath } from '@/utils/share/buildHotlinkViewerPath';
describe('buildHotlinkViewerPath', () => {
it('places upload id and secret in the viewer path', () => {
expect(buildHotlinkViewerPath('upload-uuid', 'url-safe-secret')).toBe('/h/upload-uuid/url-safe-secret');
});
});
@@ -0,0 +1 @@
export const buildHotlinkViewerPath = (uploadId: string, secret: string): string => `/h/${uploadId}/${secret}`;
@@ -0,0 +1,78 @@
import { describe, expect, it } from 'vitest';
import { config } from '@/config';
import type { UploadQueueEntry } from '@/types/upload/UploadQueueEntry';
import { buildUploadedShareUrls } from '@/utils/share/buildUploadedShareUrls';
const { clearnet, onion } = config.shareOrigins;
const uploaded = (
mode: 'zk' | 'hotlink',
overrides: Partial<Extract<UploadQueueEntry, { status: 'uploaded' }>> = {}
): UploadQueueEntry => ({
status: 'uploaded',
message: 'Uploaded',
uploadId: 'upload-id',
deleteToken: 'delete-token',
urlSecret: 'url-secret',
mode,
...overrides
});
describe('buildUploadedShareUrls', () => {
it('returns null unless the upload succeeded', () => {
const statuses = ['idle', 'queued', 'preparing', 'uploading', 'failed', 'skipped'] as const;
for (const status of statuses) {
expect(buildUploadedShareUrls({ status, message: status }, 'photo.jpg')).toBeNull();
}
});
it('builds zero-knowledge viewer, delete, and clearnet text embeds', () => {
const urls = buildUploadedShareUrls(
uploaded('zk', { deleteToken: 'a+b/c', urlSecret: 'fragment_key' }),
'a <photo> & "shot"'
);
expect(urls).toEqual({
mode: 'zk',
viewer: {
clearnet: `${clearnet}/v/upload-id#fragment_key`,
onion: `${onion}/v/upload-id#fragment_key`
},
delete: {
clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`,
onion: `${onion}/d/upload-id?token=a%2Bb%2Fc`
},
markdownLink: `[a <photo> & "shot"](${clearnet}/v/upload-id#fragment_key)`,
htmlLink: `<a href="${clearnet}/v/upload-id#fragment_key">a &lt;photo&gt; &amp; &quot;shot&quot;</a>`
});
});
it('builds hotlink viewer, direct image, delete, and clearnet image embeds', () => {
const urls = buildUploadedShareUrls(
uploaded('hotlink', { deleteToken: 'a+b/c', urlSecret: 'path_secret' }),
'a <photo> & "shot"'
);
const directClearnet = `${clearnet}/i/upload-id/path_secret`;
expect(urls).toEqual({
mode: 'hotlink',
viewer: {
clearnet: `${clearnet}/h/upload-id/path_secret`,
onion: `${onion}/h/upload-id/path_secret`
},
direct: {
clearnet: directClearnet,
onion: `${onion}/i/upload-id/path_secret`
},
delete: {
clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`,
onion: `${onion}/d/upload-id?token=a%2Bb%2Fc`
},
bbcode: `[img]${directClearnet}[/img]`,
markdownImage: `![a <photo> & "shot"](${directClearnet})`,
htmlImage: `<img src="${directClearnet}" alt="a &lt;photo&gt; &amp; &quot;shot&quot;" />`
});
});
});
@@ -8,6 +8,7 @@ import { buildMarkdownLink } from '@/utils/share/buildMarkdownLink';
import { config } from '@/config'; import { config } from '@/config';
import { buildDeletePath } from '@/utils/share/buildDeletePath'; import { buildDeletePath } from '@/utils/share/buildDeletePath';
import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath'; import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath';
import { buildHotlinkViewerPath } from '@/utils/share/buildHotlinkViewerPath';
import { buildSiteShareUrl } from '@/utils/share/buildSiteShareUrl'; import { buildSiteShareUrl } from '@/utils/share/buildSiteShareUrl';
import { buildZkViewerPath } from '@/utils/share/buildZkViewerPath'; import { buildZkViewerPath } from '@/utils/share/buildZkViewerPath';
@@ -22,23 +23,21 @@ export const buildUploadedShareUrls = (
} }
const deletePath = buildDeletePath(upload.uploadId, upload.deleteToken); const deletePath = buildDeletePath(upload.uploadId, upload.deleteToken);
const viewerPath = buildZkViewerPath(upload.uploadId, upload.urlSecret);
const viewer = {
clearnet: buildSiteShareUrl(clearnet, viewerPath),
onion: buildSiteShareUrl(onion, viewerPath)
};
const deleteUrls = { const deleteUrls = {
clearnet: buildSiteShareUrl(clearnet, deletePath), clearnet: buildSiteShareUrl(clearnet, deletePath),
onion: buildSiteShareUrl(onion, deletePath) onion: buildSiteShareUrl(onion, deletePath)
}; };
const core = { viewer, delete: deleteUrls };
if (upload.mode === 'hotlink') { if (upload.mode === 'hotlink') {
const viewerPath = buildHotlinkViewerPath(upload.uploadId, upload.urlSecret);
const directPath = buildHotlinkImagePath(upload.uploadId, upload.urlSecret); const directPath = buildHotlinkImagePath(upload.uploadId, upload.urlSecret);
const viewer = {
clearnet: buildSiteShareUrl(clearnet, viewerPath),
onion: buildSiteShareUrl(onion, viewerPath)
};
const direct = { const direct = {
clearnet: buildSiteShareUrl(clearnet, directPath), clearnet: buildSiteShareUrl(clearnet, directPath),
onion: buildSiteShareUrl(onion, directPath) onion: buildSiteShareUrl(onion, directPath)
@@ -46,7 +45,8 @@ export const buildUploadedShareUrls = (
return { return {
mode: 'hotlink', mode: 'hotlink',
...core, delete: deleteUrls,
viewer,
direct, direct,
bbcode: buildBbcodeImage(direct.clearnet), bbcode: buildBbcodeImage(direct.clearnet),
markdownImage: buildMarkdownImage(direct.clearnet, fileLabel), markdownImage: buildMarkdownImage(direct.clearnet, fileLabel),
@@ -54,9 +54,17 @@ export const buildUploadedShareUrls = (
}; };
} }
const viewerPath = buildZkViewerPath(upload.uploadId, upload.urlSecret);
const viewer = {
clearnet: buildSiteShareUrl(clearnet, viewerPath),
onion: buildSiteShareUrl(onion, viewerPath)
};
return { return {
mode: 'zk', mode: 'zk',
...core, delete: deleteUrls,
viewer,
markdownLink: buildMarkdownLink(fileLabel, viewer.clearnet), markdownLink: buildMarkdownLink(fileLabel, viewer.clearnet),
htmlLink: buildHtmlLink(fileLabel, viewer.clearnet) htmlLink: buildHtmlLink(fileLabel, viewer.clearnet)
}; };