Slave/shorten hotlink urls #15

Merged
nobswebdev merged 6 commits from slave/shorten-hotlink-urls into master 2026-11-02 14:18:07 +00:00
9 changed files with 67 additions and 26 deletions
Showing only changes of commit 0d8d682990 - Show all commits
+5
View File
@@ -35,6 +35,11 @@ const router = createRouter({
}, },
{ {
path: '/h/:id/:secret', path: '/h/:id/:secret',
component: () => import('@/views/HotlinkViewerView.vue'),
meta: { title: 'Private Image - Hotlink Viewer' }
},
{
path: '/h/:accessToken',
name: ROUTE_NAMES.HotlinkViewer, name: ROUTE_NAMES.HotlinkViewer,
component: () => import('@/views/HotlinkViewerView.vue'), component: () => import('@/views/HotlinkViewerView.vue'),
meta: { title: 'Private Image - Hotlink Viewer' } meta: { title: 'Private Image - Hotlink Viewer' }
@@ -1,11 +1,19 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath'; import {
buildHotlinkImagePath,
buildLegacyHotlinkImagePath
} from '@/utils/share/buildHotlinkImagePath';
describe('buildHotlinkImagePath', () => { describe('buildHotlinkImagePath', () => {
it('places upload id and secret in the path segments', () => { it('places the access token in a single path segment', () => {
const uploadId = 'upload-uuid'; expect(buildHotlinkImagePath('url-safe-access-token')).toBe('/i/url-safe-access-token');
const secret = 'url-safe-secret'; });
});
expect(buildHotlinkImagePath(uploadId, secret)).toBe(`/i/${uploadId}/${secret}`); describe('buildLegacyHotlinkImagePath', () => {
it('places upload id and secret in the path segments', () => {
expect(buildLegacyHotlinkImagePath('upload-uuid', 'url-safe-secret')).toBe(
'/i/upload-uuid/url-safe-secret'
);
}); });
}); });
@@ -1 +1,3 @@
export const buildHotlinkImagePath = (uploadId: string, secret: string): string => `/i/${uploadId}/${secret}`; export const buildHotlinkImagePath = (accessToken: string): string => `/i/${accessToken}`;
export const buildLegacyHotlinkImagePath = (uploadId: string, secret: string): string => `/i/${uploadId}/${secret}`;
@@ -1,8 +1,19 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { buildHotlinkViewerPath } from '@/utils/share/buildHotlinkViewerPath'; import {
buildHotlinkViewerPath,
buildLegacyHotlinkViewerPath
} from '@/utils/share/buildHotlinkViewerPath';
describe('buildHotlinkViewerPath', () => { describe('buildHotlinkViewerPath', () => {
it('places the access token in a single viewer path segment', () => {
expect(buildHotlinkViewerPath('url-safe-access-token')).toBe('/h/url-safe-access-token');
});
});
describe('buildLegacyHotlinkViewerPath', () => {
it('places upload id and secret in the viewer path', () => { it('places upload id and secret in the viewer path', () => {
expect(buildHotlinkViewerPath('upload-uuid', 'url-safe-secret')).toBe('/h/upload-uuid/url-safe-secret'); expect(buildLegacyHotlinkViewerPath('upload-uuid', 'url-safe-secret')).toBe(
'/h/upload-uuid/url-safe-secret'
);
}); });
}); });
@@ -1 +1,3 @@
export const buildHotlinkViewerPath = (uploadId: string, secret: string): string => `/h/${uploadId}/${secret}`; export const buildHotlinkViewerPath = (accessToken: string): string => `/h/${accessToken}`;
export const buildLegacyHotlinkViewerPath = (uploadId: string, secret: string): string => `/h/${uploadId}/${secret}`;
@@ -54,17 +54,17 @@ describe('buildUploadedShareUrls', () => {
'a <photo> & "shot"' 'a <photo> & "shot"'
); );
const directClearnet = `${clearnet}/i/upload-id/path_secret`; const directClearnet = `${clearnet}/i/path_secret`;
expect(urls).toEqual({ expect(urls).toEqual({
mode: 'hotlink', mode: 'hotlink',
viewer: { viewer: {
clearnet: `${clearnet}/h/upload-id/path_secret`, clearnet: `${clearnet}/h/path_secret`,
onion: `${onion}/h/upload-id/path_secret` onion: `${onion}/h/path_secret`
}, },
direct: { direct: {
clearnet: directClearnet, clearnet: directClearnet,
onion: `${onion}/i/upload-id/path_secret` onion: `${onion}/i/path_secret`
}, },
delete: { delete: {
clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`, clearnet: `${clearnet}/d/upload-id?token=a%2Bb%2Fc`,
@@ -30,8 +30,8 @@ export const buildUploadedShareUrls = (
}; };
if (upload.mode === 'hotlink') { if (upload.mode === 'hotlink') {
const viewerPath = buildHotlinkViewerPath(upload.uploadId, upload.urlSecret); const viewerPath = buildHotlinkViewerPath(upload.urlSecret);
const directPath = buildHotlinkImagePath(upload.uploadId, upload.urlSecret); const directPath = buildHotlinkImagePath(upload.urlSecret);
const viewer = { const viewer = {
clearnet: buildSiteShareUrl(clearnet, viewerPath), clearnet: buildSiteShareUrl(clearnet, viewerPath),
@@ -64,17 +64,15 @@ describe('prepareHotlinkUploadRequest', () => {
expect(recovered).toEqual(plaintext); expect(recovered).toEqual(plaintext);
}); });
it('wires secret into the hotlink path segment', async () => { it('wires secret into the hotlink direct path', async () => {
const uploadId = 'hotlink-upload-id';
const { request, secret } = await prepareHotlinkUploadRequest( const { request, secret } = await prepareHotlinkUploadRequest(
new Uint8Array([1]), new Uint8Array([1]),
'image/png', 'image/png',
options options
); );
const path = buildHotlinkImagePath(uploadId, secret); const path = buildHotlinkImagePath(secret);
expect(path).toBe(`/i/${uploadId}/${secret}`); expect(path).toBe(`/i/${secret}`);
expect(JSON.stringify(request)).not.toContain(secret); expect(JSON.stringify(request)).not.toContain(secret);
expect(path.endsWith(`/${secret}`)).toBe(true);
}); });
}); });
+21 -6
View File
@@ -28,7 +28,12 @@ import { onBeforeMount, ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { config } from '@/config'; import { config } from '@/config';
import type { HotlinkViewerViewState } from '@/types/view/HotlinkViewerViewState'; import type { HotlinkViewerViewState } from '@/types/view/HotlinkViewerViewState';
import { buildHotlinkImagePath } from '@/utils/share/buildHotlinkImagePath'; import {
buildHotlinkImagePath,
buildLegacyHotlinkImagePath
} from '@/utils/share/buildHotlinkImagePath';
const { baseUrlRoot } = config.api;
const route = useRoute(); const route = useRoute();
@@ -36,19 +41,29 @@ const state = ref<HotlinkViewerViewState>('loading');
const imageSrc = ref<string | null>(null); const imageSrc = ref<string | null>(null);
onBeforeMount(() => { onBeforeMount(() => {
const accessToken = route.params.accessToken;
const uploadId = route.params.id; const uploadId = route.params.id;
const secret = route.params.secret; const secret = route.params.secret;
if (typeof uploadId !== 'string' || typeof secret !== 'string' || !uploadId || !secret) { let path: string | null = null;
if (typeof accessToken === 'string' && accessToken) {
path = buildHotlinkImagePath(accessToken);
} else if (
typeof uploadId === 'string' &&
typeof secret === 'string' &&
uploadId &&
secret
) {
path = buildLegacyHotlinkImagePath(uploadId, secret);
}
if (!path) {
state.value = 'invalidLink'; state.value = 'invalidLink';
return; return;
} }
const { baseUrlRoot } = config.api;
const path = buildHotlinkImagePath(uploadId, secret);
imageSrc.value = `${baseUrlRoot}${path}`; imageSrc.value = `${baseUrlRoot}${path}`;
}); });