1
0
Fork 0
mirror of https://github.com/alangrainger/immich-public-proxy.git synced 2024-12-28 03:41:58 +00:00

Add #16 Download all files as zip

This commit is contained in:
Alan Grainger 2024-11-15 15:48:12 +01:00
parent 4a546419d7
commit 9e1970af4f
9 changed files with 85 additions and 16 deletions

View file

@ -5,7 +5,9 @@
}, },
"singleImageGallery": false, "singleImageGallery": false,
"singleItemAutoOpen": true, "singleItemAutoOpen": true,
"downloadOriginalPhoto": true "downloadOriginalPhoto": true,
"showGalleryTitle": false,
"allowDownloadAll": false
}, },
"lightGallery": { "lightGallery": {
"controls": true, "controls": true,

View file

@ -1,6 +1,6 @@
{ {
"name": "immich-public-proxy", "name": "immich-public-proxy",
"version": "1.4.2", "version": "1.4.3",
"scripts": { "scripts": {
"dev": "ts-node src/index.ts", "dev": "ts-node src/index.ts",
"build": "npx tsc", "build": "npx tsc",
@ -16,20 +16,22 @@
}, },
"main": "dist/index.js", "main": "dist/index.js",
"dependencies": { "dependencies": {
"express": "^4.21.1", "archiver": "^7.0.1",
"dotenv": "^16.4.5",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"dotenv": "^16.4.5",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"typescript": "^5.6.2", "express": "^4.21.1",
"tslib": "^2.8.1" "tslib": "^2.8.1",
"typescript": "^5.6.2"
}, },
"devDependencies": { "devDependencies": {
"ts-node": "^10.9.2", "@types/archiver": "^6.0.3",
"@types/node": "^16.18.111",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^16.18.111",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
"eslint": "^8.49.0", "eslint": "^8.49.0",
"eslint-config-standard": "^17.1.0" "eslint-config-standard": "^17.1.0",
"ts-node": "^10.9.2"
} }
} }

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-folder-down"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/><path d="M12 10v6"/><path d="m15 13-3 3-3-3"/></svg>

After

Width:  |  Height:  |  Size: 391 B

View file

@ -16,12 +16,13 @@ html {
margin: 4px; margin: 4px;
} }
@media (max-width:800px) { @media (max-width: 800px) {
#lightgallery a { #lightgallery a {
width: calc(100vw / 3 - 10px); width: calc(100vw / 3 - 10px);
height: calc(100vw / 3 - 10px); height: calc(100vw / 3 - 10px);
overflow: hidden; overflow: hidden;
} }
#lightgallery img { #lightgallery img {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -42,6 +43,7 @@ html {
background-repeat: no-repeat; background-repeat: no-repeat;
opacity: 0.5; opacity: 0.5;
} }
#lightgallery a:has(.play-icon):hover .play-icon { #lightgallery a:has(.play-icon):hover .play-icon {
opacity: 1; opacity: 1;
} }
@ -49,3 +51,20 @@ html {
#password { #password {
color: white; color: white;
} }
#header {
font-family: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, Helvetica, Arial, "Helvetica Neue", sans-serif;
color: white;
width: 100%;
display: flex;
}
#header h1 {
font-size: 18pt;
font-weight: bold;
margin: 4px;
}
#download-all {
margin: auto 4px auto auto;
}

View file

@ -94,8 +94,12 @@ class Immich {
return return
} }
// Everything is ok - output the link page // Everything is ok - output the shared link data
if (link.assets.length === 1) {
if (request.mode === 'download' && getConfigOption('ipp.allowDownloadAll', false)) {
// Download all assets as a zip file
await render.downloadAll(res, link)
} else if (link.assets.length === 1) {
// This is an individual item (not a gallery) // This is an individual item (not a gallery)
log('Serving link ' + request.key) log('Serving link ' + request.key)
const asset = link.assets[0] const asset = link.assets[0]

View file

@ -19,9 +19,10 @@ app.use(express.static('public', { setHeaders: addResponseHeaders }))
/* /*
* [ROUTE] This is the main URL that someone would visit if they are opening a shared link * [ROUTE] This is the main URL that someone would visit if they are opening a shared link
*/ */
app.get('/share/:key', async (req, res) => { app.get('/share/:key/:mode?', async (req, res) => {
await immich.handleShareRequest({ await immich.handleShareRequest({
key: req.params.key key: req.params.key,
mode: req.params.mode
}, res) }, res)
}) })

View file

@ -2,6 +2,7 @@ import immich from './immich'
import { Response } from 'express-serve-static-core' import { Response } from 'express-serve-static-core'
import { Asset, AssetType, ImageSize, IncomingShareRequest, SharedLink } from './types' import { Asset, AssetType, ImageSize, IncomingShareRequest, SharedLink } from './types'
import { getConfigOption } from './functions' import { getConfigOption } from './functions'
import archiver from 'archiver'
class Render { class Render {
lgConfig lgConfig
@ -111,6 +112,9 @@ class Render {
items, items,
openItem, openItem,
title: this.title(share), title: this.title(share),
path: '/share/' + share.key,
showDownload: getConfigOption('ipp.allowDownloadAll', true),
showTitle: getConfigOption('ipp.showGalleryTitle', true),
lgConfig: getConfigOption('lightGallery', {}) lgConfig: getConfigOption('lightGallery', {})
}) })
} }
@ -121,6 +125,27 @@ class Render {
title (share: SharedLink) { title (share: SharedLink) {
return share.description || share?.album?.albumName || '' return share.description || share?.album?.albumName || ''
} }
/**
* Download all assets as a zip file
*/
async downloadAll (res: Response, share: SharedLink) {
res.setHeader('Content-Type', 'application/zip')
const title = this.title(share).replace(/[^\w .-]/g, '') + '.zip'
res.setHeader('Content-Disposition', `attachment; filename="${title}"`)
const archive = archiver('zip', { zlib: { level: 9 } })
archive.pipe(res)
for (const asset of share.assets) {
const url = immich.buildUrl(immich.apiUrl() + '/assets/' + encodeURIComponent(asset.id) + '/original', {
key: asset.key,
password: asset.password
})
const data = await fetch(url)
archive.append(Buffer.from(await data.arrayBuffer()), { name: asset.originalFileName || asset.id })
}
await archive.finalize()
res.end()
}
} }
const render = new Render() const render = new Render()

View file

@ -6,6 +6,7 @@ export enum AssetType {
export interface Asset { export interface Asset {
id: string; id: string;
key: string; key: string;
originalFileName?: string;
password?: string; password?: string;
type: AssetType; type: AssetType;
isTrashed: boolean; isTrashed: boolean;
@ -39,6 +40,7 @@ export enum ImageSize {
export interface IncomingShareRequest { export interface IncomingShareRequest {
key: string; key: string;
password?: string; password?: string;
mode?: string;
size?: ImageSize; size?: ImageSize;
range?: string; range?: string;
} }

View file

@ -7,6 +7,16 @@
<link type="text/css" rel="stylesheet" href="/lightgallery-bundle.min.css"/> <link type="text/css" rel="stylesheet" href="/lightgallery-bundle.min.css"/>
</head> </head>
<body> <body>
<div id="header">
<% if (showTitle) { %>
<h1><%- title || 'Gallery' %></h1>
<% } %>
<% if (showDownload) { %>
<div id="download-all">
<a href="<%- path %>/download" title="Download all"><img src="/images/download-all.svg" height="24" width="24" alt="Download all"></a>
</div>
<% } %>
</div>
<div id="lightgallery"> <div id="lightgallery">
<% items.forEach(item => { <% items.forEach(item => {
if (item.video) { %> if (item.video) { %>
@ -15,8 +25,11 @@
<div class="play-icon"></div> <div class="play-icon"></div>
</a> </a>
<% } else { %> <% } else { %>
<a href="<%- item.previewUrl %>"<% if (item.downloadUrl) { %> <a href="<%- item.previewUrl %>"
data-download-url="<%- item.downloadUrl %>"<% } %>> <% if (item.downloadUrl) { %>
data-download-url="<%- item.downloadUrl %>"
<% } %>
>
<img alt="" src="<%- item.thumbnailUrl %>"/> <img alt="" src="<%- item.thumbnailUrl %>"/>
</a> </a>
<% } <% }