Merge branch 'thumbsup:master' into master

pull/284/head
dravenst 1 year ago committed by GitHub
commit 74bd15ee65
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
nodejs: [12, 14]
nodejs: [14, 16, 18]
steps:
- uses: actions/checkout@v2
- uses: docker/setup-qemu-action@v1

@ -1,6 +1,7 @@
name: Test on Linux
on:
workflow_dispatch:
push:
pull_request:
branches: [master]

@ -7,8 +7,7 @@
<!-- Build status and code analysis -->
![Linux Tests](https://github.com/thumbsup/thumbsup/actions/workflows/test-linux.yml/badge.svg)
[![Dependencies](http://img.shields.io/david/thumbsup/thumbsup.svg?style=flat)](https://david-dm.org/thumbsup/thumbsup)
[![Dev dependencies](https://david-dm.org/thumbsup/thumbsup/dev-status.svg?style=flat)](https://david-dm.org/thumbsup/thumbsup?type=dev)
![Dependencies](https://img.shields.io/librariesio/release/npm/thumbsup)
<!-- Social sharing -->
[![Twitter](https://img.shields.io/badge/share-Twitter-1CA8F5.svg)](https://twitter.com/intent/tweet?text=Need%20static%20photo%20and%20video%20galleries?%20Check%20out%20Thumbsup%20on%20Github&url=https://github.com/thumbsup/thumbsup&hashtags=selfhosted,static,gallery)

@ -11,9 +11,4 @@ FROM ghcr.io/thumbsup/runtime:node-${NODE_VERSION}
LABEL org.opencontainers.image.source https://github.com/thumbsup/thumbsup
# Standard build dependencies for npm install
RUN apk add --no-cache git make g++ python bash
# Pre-install expensive dependencies
WORKDIR /app
RUN npm init -y
RUN npm install better-sqlite3
RUN apk add --no-cache git make g++ python3 bash

@ -2,7 +2,7 @@
# Builder image
# ------------------------------------------------
FROM ghcr.io/thumbsup/build:node-12 as build
FROM ghcr.io/thumbsup/build:node-18 as build
# Install thumbsup locally
WORKDIR /thumbsup
@ -18,7 +18,7 @@ RUN npm install thumbsup@${PACKAGE_VERSION}
# Runtime image
# ------------------------------------------------
FROM ghcr.io/thumbsup/runtime:node-12
FROM ghcr.io/thumbsup/runtime:node-18
# Use tini as an init process
# to ensure all child processes (ffmpeg...) are always terminated properly

@ -1,5 +1,6 @@
# Node.js + build dependencies + runtime dependencies
FROM ghcr.io/thumbsup/build:node-12
FROM ghcr.io/thumbsup/build:node-18
WORKDIR /app
# Switch to a non-root user
# So we can test edge-cases around file permissions

9955
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -36,49 +36,49 @@
"@thumbsup/theme-classic": "^1.2.0",
"@thumbsup/theme-flow": "^1.3.0",
"@thumbsup/theme-mosaic": "^1.2.0",
"JSONStream": "^1.3.5",
"async": "^3.2.0",
"better-sqlite3": "^7.1.5",
"async": "^3.2.4",
"better-sqlite3": "^7.6.2",
"boxen": "^5.0.1",
"chalk": "^4.1.0",
"command-exists": "^1.2.9",
"debug": "^4.3.1",
"debug": "^4.3.4",
"event-stream": "^4.0.1",
"fs-extra": "^9.1.0",
"fs-readdir-recursive": "^1.0.0",
"handlebars": "^4.7.7",
"ini": "^1.3.4",
"insight": "^0.10.3",
"less": "^4.1.1",
"ini": "^3.0.1",
"insight": "^0.11.1",
"JSONStream": "^1.3.5",
"less": "^4.1.3",
"listr": "^0.14.3",
"lodash": "^4.17.21",
"micromatch": "^4.0.4",
"moment": "^2.29.1",
"micromatch": "^4.0.5",
"moment": "^2.29.4",
"readdir-enhanced": "^6.0.4",
"resolve-pkg": "^2.0.0",
"slugify": "^1.5.0",
"slugify": "^1.6.5",
"through2": "^4.0.2",
"thumbsup-downsize": "^2.4.3",
"url-join": "^4.0.1",
"yargs": "^16.2.0",
"yargs": "^17.5.1",
"zen-observable": "^0.8.15"
},
"devDependencies": {
"glob": "^7.1.6",
"glob": "^8.0.3",
"gm": "^1.23.1",
"injectmd": "^1.0.0",
"markdown-toc": "^1.1.0",
"mocha": "^8.3.2",
"mock-fs": "^4.13.0",
"mocha": "^10.0.0",
"mock-fs": "^5.1.4",
"mock-spawn": "^0.2.6",
"nyc": "^15.1.0",
"require-lint": "^2.0.2",
"require-lint": "^2.0.3",
"should": "^13.2.3",
"sinon": "^10.0.0",
"sinon": "^14.0.0",
"standard": "^12.0.1",
"stream-mock": "^2.0.5",
"tmp": "^0.2.1",
"yaml": "^1.10.2"
"yaml": "^2.1.1"
},
"standard": {
"ignore": [

@ -18,6 +18,13 @@ const BINARIES = [
url: 'http://www.graphicsmagick.org',
msg: ''
},
{
// optional to process HEIC files
mandatory: false,
cmd: 'magick',
url: 'https://imagemagick.org',
msg: 'You will not be able to process HEIC images.'
},
{
// optional to process videos
mandatory: false,

@ -2,22 +2,22 @@ const _ = require('lodash')
const path = require('path')
const Album = require('../model/album')
exports.createAlbums = function (collection, mapper, opts) {
exports.createAlbums = function (collection, mapper, opts, picasaReader) {
// returns a top-level album for the home page
// under which all files are grouped into sub-albums
// and finalised recursively (calculate stats, etc...)
const home = group(collection, mapper, opts.homeAlbumName)
const home = group(collection, mapper, opts, picasaReader)
home.finalize(opts)
return home
}
function group (collection, mapper, homeAlbumName) {
function group (collection, mapper, opts, picasaReader) {
// this hashtable will contain all albums, with the full path as key
// e.g. groups['holidays/tokyo']
var groups = {
// the home album is indexed as '.'
// the value of '.' is local to this function, and doesn't appear anywhere else
'.': new Album(homeAlbumName)
'.': new Album(opts.homeAlbumName)
}
// put all files in the right albums
// a file can be in multiple albums
@ -29,7 +29,7 @@ function group (collection, mapper, homeAlbumName) {
.uniq()
.value()
albums.forEach(albumPath => {
createAlbumHierarchy(groups, albumPath)
createAlbumHierarchy(groups, albumPath, opts, picasaReader)
groups[albumPath].files.push(file)
})
})
@ -37,17 +37,27 @@ function group (collection, mapper, homeAlbumName) {
return groups['.']
}
function createAlbumHierarchy (allGroupNames, segment) {
function createAlbumHierarchy (allGroupNames, segment, opts, picasaReader) {
if (!allGroupNames.hasOwnProperty(segment)) {
// create parent albums first
var parent = path.dirname(segment)
const parent = path.dirname(segment)
if (parent !== '.') {
createAlbumHierarchy(allGroupNames, parent)
createAlbumHierarchy(allGroupNames, parent, opts, picasaReader)
}
const picasaName = getPicasaName(segment, opts, picasaReader)
const lastSegment = path.basename(segment)
const title = picasaName || lastSegment
// then create album if it doesn't exist
// and attach it to its parent
var lastSegment = path.basename(segment)
allGroupNames[segment] = new Album({ title: lastSegment })
allGroupNames[segment] = new Album({ title })
allGroupNames[parent].albums.push(allGroupNames[segment])
}
}
function getPicasaName (segment, opts, picasaReader) {
const fullPath = path.join(opts.input, segment)
const picasaFile = picasaReader.album(fullPath)
return picasaFile != null ? picasaFile.name : null
}

@ -10,37 +10,32 @@ const path = require('path')
class Picasa {
constructor () {
// memory cache of all Picasa files read so far
this.folders = {}
}
album (dir) {
if (!this.folders[dir]) {
this.folders[dir] = loadPicasa(dir)
}
const entry = this.folderMetadata(dir)
// album metadata is stored in a section called [Picasa]
const entry = this.folders[dir]
return entry.Picasa || null
}
file (filepath) {
const dir = path.dirname(filepath)
if (!this.folders[dir]) {
this.folders[dir] = loadPicasa(dir)
}
const entry = this.folderMetadata(dir)
// file metadata is stored in a section called [FileName.ext]
const entry = this.folders[dir]
const filename = path.basename(filepath)
const fileParts = filename.split('.')
return getIniValue(entry, fileParts)
}
}
function loadPicasa (dirname) {
const inipath = path.join(dirname, 'picasa.ini')
const content = loadIfExists(inipath)
if (!content) {
// return an empty hash, as if the picasa.ini file existed but was empty
return {}
} else {
return ini.parse(content)
folderMetadata (dirname) {
// try reading from cache first
if (this.folders[dirname]) {
return this.folders[dirname]
}
// otherwise try to read the file from disk
const inipath = path.join(dirname, 'picasa.ini')
const content = loadIfExists(inipath)
this.folders[dirname] = content ? ini.parse(content) : {}
return this.folders[dirname]
}
}

@ -51,7 +51,7 @@ exports.run = function (opts, callback) {
// finished, we can create the albums
emitter.on('done', stats => {
const mapper = new AlbumMapper(opts.albumsFrom, opts)
const album = hierarchy.createAlbums(files, mapper, opts)
const album = hierarchy.createAlbums(files, mapper, opts, picasaReader)
callback(null, files, album)
observer.complete()
})

@ -1,10 +1,13 @@
const path = require('path')
const should = require('should/as-function')
const sinon = require('sinon')
const hierarchy = require('../../src/input/hierarchy.js')
const Album = require('../../src/model/album.js')
const fixtures = require('../fixtures')
const Picasa = require('../../src/input/picasa')
const DEFAULT_OPTS = { homeAlbumName: 'Home' }
const DEFAULT_OPTS = { homeAlbumName: 'Home', input: '' }
const picasaReader = new Picasa()
describe('hierarchy', function () {
beforeEach(function () {
@ -14,7 +17,7 @@ describe('hierarchy', function () {
describe('root album', function () {
it('creates a root album (homepage) to put all sub-albums', function () {
const mapper = mockMapper(file => ['all'])
const home = hierarchy.createAlbums([], mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums([], mapper, DEFAULT_OPTS, picasaReader)
should(home.title).eql('Home')
})
@ -26,7 +29,7 @@ describe('hierarchy', function () {
it('defaults the homepage to index.html', function () {
const mapper = mockMapper(file => ['all'])
const home = hierarchy.createAlbums([], mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums([], mapper, DEFAULT_OPTS, picasaReader)
should(home.path).eql('index.html')
should(home.url).eql('index.html')
})
@ -48,7 +51,7 @@ describe('hierarchy', function () {
fixtures.photo({ path: 'IMG_000002.jpg' })
]
const mapper = mockMapper(file => [value])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.albums.length).eql(0)
should(home.files.length).eql(2)
should(home.files[0].filename).eql('IMG_000001.jpg')
@ -58,13 +61,33 @@ describe('hierarchy', function () {
})
describe('nested albums', function () {
it('uses album title from Picasa file if available', function () {
const files = [
fixtures.photo({ path: 'IMG_000001.jpg' })
]
const mapper = mockMapper(file => ['all'])
const opts = { ...DEFAULT_OPTS, input: '/root' }
const expectedPath = path.join(opts.input, 'all')
const picasa = new Picasa()
sinon.stub(picasa, 'album').withArgs(expectedPath)
.returns({ name: 'picasa-name' })
const home = hierarchy.createAlbums(files, mapper, opts, picasa)
should(home.albums.length).eql(1)
should(home.albums[0].title).eql('picasa-name')
should(home.albums[0].files).eql([files[0]])
})
it('can group media into a single folder', function () {
const files = [
fixtures.photo({ path: 'IMG_000001.jpg' }),
fixtures.photo({ path: 'IMG_000002.jpg' })
]
const mapper = mockMapper(file => ['all'])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.albums.length).eql(1)
should(home.albums[0].title).eql('all')
should(home.albums[0].files).eql([files[0], files[1]])
@ -76,7 +99,7 @@ describe('hierarchy', function () {
fixtures.photo({ path: 'two/IMG_000002.jpg' })
]
const mapper = mockMapper(file => [path.dirname(file.path)])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.albums.length).eql(2)
should(home.albums[0].title).eql('one')
should(home.albums[0].files).eql([files[0]])
@ -90,7 +113,7 @@ describe('hierarchy', function () {
fixtures.photo({ path: 'IMG_000002.jpg' })
]
const mapper = mockMapper(file => ['one/two'])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.albums.length).eql(1)
should(home.albums[0].title).eql('one')
should(home.albums[0].albums.length).eql(1)
@ -104,7 +127,7 @@ describe('hierarchy', function () {
fixtures.photo({ path: 'one/two/IMG_000002.jpg' })
]
const mapper = mockMapper(file => [path.dirname(file.path)])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.albums.length).eql(1)
should(home.albums[0].title).eql('one')
should(home.albums[0].files).eql([files[0]])
@ -118,7 +141,7 @@ describe('hierarchy', function () {
fixtures.photo({ path: 'one/IMG_000001.jpg' })
]
const mapper = mockMapper(file => ['.', '/', path.dirname(file.path)])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.files.length).eql(1)
should(home.files[0].filename).eql(files[0].filename)
should(home.albums.length).eql(1)
@ -132,7 +155,7 @@ describe('hierarchy', function () {
fixtures.photo({ path: 'one/IMG_000001.jpg' })
]
const mapper = mockMapper(file => ['one', path.dirname(file.path)])
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS)
const home = hierarchy.createAlbums(files, mapper, DEFAULT_OPTS, picasaReader)
should(home.albums.length).eql(1)
should(home.albums[0].title).eql('one')
should(home.albums[0].files).eql(files)

@ -1,3 +1,5 @@
const fs = require('fs')
const sinon = require('sinon')
const should = require('should/as-function')
const Picasa = require('../../src/input/picasa.js')
@ -29,7 +31,7 @@ describe('Picasa', function () {
})
const picasa = new Picasa()
const meta = picasa.album('holidays')
should(meta).eql({
should(meta).have.properties({
name: 'My holidays'
})
})
@ -38,13 +40,21 @@ describe('Picasa', function () {
const meta = picasa.album('holidays')
should(meta).eql(null)
})
it('returns <null> if the Picasa file is invalid', function () {
mock({
'holidays/picasa.ini': '[[invalid'
})
const picasa = new Picasa()
const meta = picasa.album('holidays')
should(meta).eql(null)
})
it('returns raw file metadata as read from the INI file', function () {
mock({
'holidays/picasa.ini': PICASA_INI
})
const picasa = new Picasa()
const meta = picasa.file('holidays/IMG_0001.jpg')
should(meta).eql({
should(meta).have.properties({
star: 'yes',
caption: 'Nice sunset',
keywords: 'beach,sunset'
@ -56,7 +66,7 @@ describe('Picasa', function () {
})
const picasa = new Picasa()
const meta = picasa.file('holidays/IMG.0001.small.jpg')
should(meta).eql({
should(meta).have.properties({
caption: 'dots'
})
})
@ -65,7 +75,19 @@ describe('Picasa', function () {
'holidays/picasa.ini': PICASA_INI
})
const picasa = new Picasa()
const meta = picasa.album('holidays/IMG_0002.jpg')
const meta = picasa.file('holidays/IMG_0002.jpg')
should(meta).eql(null)
})
it('only reads the file from disk once', function () {
mock({
'holidays/picasa.ini': PICASA_INI
})
sinon.spy(fs, 'readFileSync')
const picasa = new Picasa()
picasa.album('holidays')
picasa.album('holidays')
picasa.file('holidays/IMG_0001.jpg')
picasa.file('holidays/IMG_0002.jpg')
should(fs.readFileSync.callCount).eql(1)
})
})

Loading…
Cancel
Save