Christian von Arnim 4 months ago committed by GitHub
commit 32a04e9ace
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -245,11 +245,10 @@ const OPTIONS = {
type: 'number',
'default': null
},
'album-zip-files': {
'album-download': {
group: 'Album options:',
description: 'Create a ZIP file per album',
type: 'boolean',
'default': false
description: 'How albums can be downloaded (\'js\' is experimental)',
choices: ['js', 'zip']
},
// 'keyword-fields': {
// group: 'Album options:',
@ -442,8 +441,12 @@ const OPTIONS = {
group: 'Deprecated:',
description: 'Enable anonymous usage statistics',
type: 'boolean'
}
},
'album-zip-files': {
group: 'Deprecated:',
description: 'Create a ZIP file per album, set album-download to \'zip\'',
type: 'boolean'
},
}
// explicitly pass <process.argv> so we can unit test this logic
@ -509,6 +512,9 @@ exports.get = (args, exitOnFailure = true) => {
// Convert deprecated --css
if (opts.css) opts.themeStyle = opts.css
// Convert deprecated --album-zip-files
if(opts.albumZipFiles) opts.albumDownload = 'zip'
// Add a dash prefix to any --gm-args value
// We can't specify the prefix on the CLI otherwise the parser thinks it's a thumbsup arg
if (opts.gmArgs) {

@ -32,9 +32,17 @@ exports.build = function (opts, done) {
}
}
},
{
title: 'Get file sizes',
enabled: (ctx) => true,
skip: () => opts.dryRun,
task: (ctx, task) => {
return steps.fileSize(ctx.files, opts)
}
},
{
title: 'Updating ZIP files',
enabled: (ctx) => opts.albumZipFiles,
enabled: (ctx) => opts.albumDownload === 'zip',
skip: () => opts.dryRun,
task: (ctx) => {
return steps.zipAlbums(ctx.album, opts.output)

@ -65,7 +65,7 @@ Album.prototype.finalize = function (options, parent) {
this.depth = parent.depth + 1
}
// path to the optional ZIP file
if (options.albumZipFiles && this.files.length > 0) {
if (options.albumDownload === 'zip' && this.files.length > 0) {
this.zip = this.path.replace(/\.[^\\/.]+$/, '.zip')
}
// then finalize all nested albums (which uses the parent basename)

@ -2,3 +2,4 @@ exports.index = require('./step-index').run
exports.process = require('./step-process').run
exports.cleanup = require('./step-cleanup').run
exports.zipAlbums = require('./step-album-zip').run
exports.fileSize = require('./step-file-size').run

@ -0,0 +1,19 @@
const path = require('node:path')
const fs = require('node:fs')
const Observable = require('zen-observable')
const trace = require('debug')('thumbsup:trace')
const debug = require('debug')('thumbsup:debug')
const error = require('debug')('thumbsup:error')
exports.run = function (files, opts) {
return new Observable(observer => {
files.map(f => {
filePath = path.join(opts.output, f.output.download.path)
stats = fs.statSync(filePath)
f.fileSize = {download: stats.size}
})
observer.complete()
})
}

@ -0,0 +1,9 @@
<html>
<head>
<title>No theme applied js-download</title>
</head>
<body>
<h1>No theme applied js-download<h1>
<p>This is just the js-download base which contains common files for album-download == 'js' </p>
</body>
</html>

@ -0,0 +1,44 @@
{{!-- Do not include this internal partial directly, include js-download partial instead --}}
<!-- JS Download -->
<div class="js-download" id="jsDownload">
JS Download not supported in this browser.
Requires https and a compatible <a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/showDirectoryPicker#browser_compatibility">browser</a>, e.g. Chrome on Desktop
</div>
<script type="module">
import {downloadImages} from './{{relative 'public/downloadImages.js'}}'
var download_images = [
{{#each album.files}}
{ url: "{{relative urls.download}}", size: {{fileSize.download}} },
{{/each}}
]
const divEl = document.getElementById('jsDownload')
const statusDiv = document.createElement('div')
statusDiv.classList?.add('status')
const download = async (override) => {
const downloadResult = await downloadImages(download_images, statusDiv, override)
// Log for debugging while experimental
console.log(downloadResult)
}
if(window.showDirectoryPicker !== undefined)
{
const dlDiv = document.createElement('div')
dlDiv.classList.add('download')
dlDiv.innerHTML = '<label for="dlOverride">Override existing files?</label>'
const overrideInput = document.createElement('input')
overrideInput.type = 'checkbox';
overrideInput.id = 'dlOverride'
dlDiv.appendChild(overrideInput)
const button = document.createElement('button')
button.innerHTML = 'Download all'
button.addEventListener('click', () => download(overrideInput.checked))
dlDiv.appendChild(button)
divEl.replaceChildren(dlDiv, statusDiv)
}
</script>

@ -0,0 +1,74 @@
//Taken from https://github.com/ccvca/js-multi-file-download/blob/bcafa4a947ec19286c217231b6383a0e6bea44fc/HtmlJsExample/downloadImages.js
import { DownloadFiles, FileState } from './multi-file-download/multi-file-download.js'
// Download images and show result in a div
export async function downloadImages(imageList, statusDiv, overrideExistingFile) {
const dirPickOpts = {
mode: 'readwrite',
//startIn: 'pictures'
};
const dirHandle = await window.showDirectoryPicker(dirPickOpts);
// Setup HTML Status display
let dlFiles = {};
const summary = document.createElement('div');
summary.classList.add('summary');
statusDiv.replaceChildren(summary); // Clear all (old) childs
const updateSummary = () => {
const imgCnt = Object.keys(dlFiles).reduce(
(accumulator, url) => dlFiles[url]?.state !== FileState.STARTED ? accumulator + 1 : accumulator
, 0
);
summary.innerHTML = `${imgCnt} / ${imageList.length}`;
};
updateSummary();
// Callback with download progress
const onStateUpdate = (url, state) => {
let element;
if (url in dlFiles) {
element = dlFiles[url];
element = { ...element, ...state };
}
else if (state.state !== undefined) {
element = { state: state.state, ...state };
element.htmlEl = document.createElement('div');
element.htmlEl.classList.add('progress')
statusDiv.appendChild(element.htmlEl);
} else {
console.error('Stored DlFilesState broken.');
return;
}
let text = `${url}`;
if (element.state != FileState.STARTED) {
text += ` ${FileState[element.state]}`
}
if (state.progress?.percent !== undefined) {
text += ` ${+state.progress?.percent.toFixed(1)} %`;
}
if (state.error) {
element.htmlEl.classList.add('error');
console.error(state.error);
text += ` ${state.error.message}`
}
element.htmlEl.innerHTML = text;
if (element.state === FileState.COMPLETED_DOWNLOAD || element.state === FileState.SKIPPED_EXIST){
statusDiv.removeChild(element.htmlEl);
}
dlFiles = { ...dlFiles, [url]: element };
updateSummary();
};
await DownloadFiles(dirHandle, imageList, {
onStateUpdate: onStateUpdate,
overrideExistingFile: overrideExistingFile
});
// Return final state
return dlFiles;
};

@ -0,0 +1,159 @@
var m = Object.defineProperty;
var v = (e, t, r) => t in e ? m(e, t, { enumerable: !0, configurable: !0, writable: !0, value: r }) : e[t] = r;
var x = (e, t, r) => (v(e, typeof t != "symbol" ? t + "" : t, r), r);
var s;
((e) => {
class t extends Error {
constructor(a) {
super(a);
}
}
e.MetadataError = t;
class r extends Error {
constructor(a) {
super(a);
}
}
e.DownloadError = r;
class l extends Error {
constructor(a) {
super(a);
}
}
e.FileExistError = l;
class u extends Error {
constructor(a) {
super(a);
}
}
e.InternalError = u;
class c extends Error {
constructor(n, d) {
super(n);
x(this, "innerException");
this.innerException = d;
}
}
e.GeneralError = c;
})(s || (s = {}));
function b(e) {
return new URL(e, window.location.href).pathname.split("/").pop();
}
async function D(e, t, r) {
try {
return (await (await e.getFileHandle(t)).getFile()).size === r;
} catch {
return !1;
}
}
var y = /* @__PURE__ */ ((e) => (e[e.SKIPPED_EXIST = 1] = "SKIPPED_EXIST", e[e.DOWNLOADED = 2] = "DOWNLOADED", e))(y || {});
async function S(e, t, r = {}) {
const l = t.fileName === void 0 ? b(t.url) : t.fileName;
if (l === void 0)
throw new s.MetadataError("Could not determine filename.");
if (t.size !== void 0 && await D(e, l, t.size))
return 1;
if (r.overrideExistingFile !== !0)
try {
throw await e.getFileHandle(l, { create: !1 }), new s.FileExistError(`File: ${l} does already exist.`);
} catch (a) {
const n = a;
if (a instanceof s.FileExistError)
throw a;
if (n.name === void 0 || n.name !== "NotFoundError")
throw new s.FileExistError(`File: ${l} does already exist. Exeption: ${n.message}`);
}
const u = new AbortController(), c = await fetch(t.url, { signal: u.signal });
if (!c.ok)
throw new s.DownloadError(`Error while downloading: ${c.status} - ${c.statusText}`);
if (c.body === null)
throw new s.DownloadError("No data");
let o = c.body;
if (r.progress !== void 0) {
let a = 0;
const n = c.headers.get("content-length"), d = Number.parseInt(n ?? "") || void 0, f = new TransformStream(
{
transform(w, E) {
a += w.length;
let i = d !== void 0 ? a / d * 100 : void 0;
if (r.progress !== void 0) {
try {
r.progress(a, d, i);
} catch (g) {
console.log(g);
}
E.enqueue(w);
}
}
}
);
o = o.pipeThrough(f);
}
try {
const n = await (await e.getFileHandle(l, { create: !0 })).createWritable();
await o.pipeTo(n);
} catch (a) {
throw u.abort(), new s.GeneralError(`Download of file ${l} failed due to an exception: ${a == null ? void 0 : a.message}`, a);
}
return 2;
}
var T = /* @__PURE__ */ ((e) => (e[e.STARTED = 0] = "STARTED", e[e.COMPLETED_DOWNLOAD = 1] = "COMPLETED_DOWNLOAD", e[e.SKIPPED_EXIST = 2] = "SKIPPED_EXIST", e[e.ERROR = 3] = "ERROR", e))(T || {});
async function I(e, t, r) {
var c, o, a, n;
r === void 0 && (r = {});
const l = new AbortController(), u = r.abortSignal === void 0 ? l.signal : r.abortSignal;
for (const d of t) {
if (u.aborted)
break;
const f = r.onStateUpdate === void 0 ? void 0 : (E, i, g) => {
var h;
(h = r == null ? void 0 : r.onStateUpdate) == null || h.call(r, d.url, {
progress: {
bytes: E,
totalBytes: i,
percent: g
}
});
}, w = {
overrideExistingFile: r.overrideExistingFile,
progress: f
};
(c = r == null ? void 0 : r.onStateUpdate) == null || c.call(r, d.url, {
state: 0
/* STARTED */
});
try {
const E = await S(e, d, w);
switch (E) {
case 2:
(o = r == null ? void 0 : r.onStateUpdate) == null || o.call(r, d.url, {
state: 1
/* COMPLETED_DOWNLOAD */
});
break;
case 1:
(a = r == null ? void 0 : r.onStateUpdate) == null || a.call(r, d.url, {
state: 2
/* SKIPPED_EXIST */
});
break;
default:
throw new s.InternalError(`Unknown return value from download function: ${E} `);
}
} catch (E) {
const i = E;
(n = r == null ? void 0 : r.onStateUpdate) == null || n.call(r, d.url, {
state: 3,
error: i
});
}
}
}
export {
S as DownloadFile,
y as DownloadFileRet,
I as DownloadFiles,
s as Exceptions,
T as FileState,
D as VerifyFileSize
};

@ -0,0 +1,27 @@
/* Default style for js-download.
Use {{> js-download-default-style}} in album.hbs to include it.
*/
#jsDownload {
div.download {
font-size: larger;
label, input, button {
margin-left: .5rem;
}
button {
border-width: 1pt;
background-color: lightgrey;
text-decoration: none;
}
}
div.status {
margin-left: .5rem;
div.summary{
font-size: larger;
}
div.error{
color: red;
}
}
}

@ -0,0 +1,3 @@
{{#compare @root.gallery.albumDownload '==' 'js'}}
<link rel="stylesheet" href="{{relative 'public/jsDownload.css'}}" />
{{/compare}}

@ -0,0 +1,4 @@
{{#compare @root.gallery.albumDownload '==' 'js'}}
{{!-- Include the code from 'theme-base-js-download' --}}
{{> js-download-internal}}
{{/compare}}

@ -14,6 +14,12 @@ exports.build = function (rootAlbum, opts, callback) {
stylesheetName: 'core.css'
})
// Shared JS libs for js-download
const baseJsDownloadDir = path.join(__dirname, 'theme-base-js-download')
const baseJsDownload = new Theme(baseJsDownloadDir, opts.output, {
stylesheetName: 'jsDownload.css'
})
// then create the actual theme assets
const themeDir = opts.themePath || localThemePath(opts.theme)
const theme = new Theme(themeDir, opts.output, {
@ -33,6 +39,7 @@ exports.build = function (rootAlbum, opts, callback) {
// now build everything
async.series([
next => base.prepare(next),
... opts.albumDownload === 'js' ? [next => baseJsDownload.prepare(next)]: [],
next => theme.prepare(next),
next => async.series(tasks, next)
], callback)

@ -278,7 +278,7 @@ describe('Album', function () {
it('is undefined if the album has no direct files', function () {
const a = new Album('Holidays')
a.finalize({ albumZipFiles: true })
a.finalize({ albumDownload: 'zip' })
should(a.zip).eql(undefined)
})
@ -290,7 +290,7 @@ describe('Album', function () {
fixtures.photo({ path: 'b' })
]
})
a.finalize({ albumZipFiles: true })
a.finalize({ albumDownload: 'zip' })
should(a.zip).eql('index.zip')
})
@ -306,7 +306,7 @@ describe('Album', function () {
title: 'Holidays',
albums: [london]
})
root.finalize({ albumZipFiles: true })
root.finalize({ albumDownload: 'zip' })
should(london.zip).eql('London.zip')
})
})

Loading…
Cancel
Save