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:
parent
4a546419d7
commit
9e1970af4f
9 changed files with 85 additions and 16 deletions
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
app/public/images/download-all.svg
Normal file
1
app/public/images/download-all.svg
Normal 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 |
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
<% }
|
<% }
|
||||||
|
|
Loading…
Reference in a new issue