diff --git a/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to .github/CODE_OF_CONDUCT.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 03b9ba42..22e63a26 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,25 +1,82 @@ name: Node.js CI -#on: [push] -on: [workflow_dispatch] +on: + workflow_dispatch: + push: + branches: + - development + - staging + - master jobs: build: runs-on: ubuntu-latest strategy: + max-parallel: 6 matrix: - node-version: [8.x, 10.x, 12.x] + node-version: [10.x, 12.x] steps: - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + + # Use nodejs cache + - name: Cache Node.js modules + uses: actions/cache@v2 + with: + # npm cache files are stored in `~/.npm` on Linux/macOS + path: ~/.npm + key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.OS }}-node- + ${{ runner.OS }}- + + # Enable conversion to PDF + - run: sudo sed -i 's/policy domain="coder" rights="none" pattern="PDF"/policy domain="coder" rights="read | write" pattern="PDF"'/ /etc/ImageMagick-6/policy.xml + + # Install packages and build - run: npm install + - run: npm run server-lint - run: npm run server-build - run: npm run client-build - run: npm run test env: CI: true + - run: npm run package + + # We may need the package name for a release later (if not the dev branch and it's node 12) + - name: Retrieve package name + if: endsWith(matrix.node-version, '12.x') && !endsWith(github.ref, 'development') + run: | + echo "PACKAGE_NAME=$(ls ./release/)" >> $GITHUB_ENV + + # Create a draft release if there's a package + - name: Create Release + id: create_release + if: env.PACKAGE_NAME + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ github.ref }} + release_name: Release ${{ github.ref }} + draft: true + prerelease: true + + # Upload the release package + - name: Upload Release Asset + id: upload-release-asset + if: env.PACKAGE_NAME + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} + asset_path: ./release/${{ env.PACKAGE_NAME }} + asset_name: ${{ env.PACKAGE_NAME }} + asset_content_type: application/gzip diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e111d9ee..c238e590 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -7,14 +7,14 @@ name: "CodeQL" on: push: - branches: [master] + branches: [master, staging] pull_request: # The branches below must be a subset of the branches above - branches: [master] -# schedule: -# - cron: '0 7 * * 2' + branches: [master, staging] workflow_dispatch: - + #schedule: + # - cron: '0 7 * * 2' + jobs: analyze: name: Analyze diff --git a/.gitignore b/.gitignore index e3a667a9..13c69989 100644 --- a/.gitignore +++ b/.gitignore @@ -96,7 +96,8 @@ dist/**/* *.tif *.pdf *.png -data/output/*.txt +data/output/* +data/temp/* scan*.jpg config/devices.json var diff --git a/Dockerfile b/Dockerfile index 0ab07252..b20334d9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,22 +5,38 @@ WORKDIR "$APP_DIR" COPY package*.json "$APP_DIR/" RUN npm install COPY . "$APP_DIR" -RUN npm run server-build -RUN npm run client-build +RUN npm run server-build && npm run client-build # production image FROM node:buster-slim ENV APP_DIR=/app WORKDIR "$APP_DIR" -# Install sane -RUN apt-get update && apt-get install -yq sane sane-utils imagemagick -RUN sed -i 's/policy domain="coder" rights="none" pattern="PDF"/policy domain="coder" rights="read | write" pattern="PDF"'/ /etc/ImageMagick-6/policy.xml +RUN apt-get update && \ + apt-get install -yq curl gpg && \ + echo 'deb http://download.opensuse.org/repositories/home:/pzz/Debian_10/ /' | tee /etc/apt/sources.list.d/home:pzz.list && \ + curl -fsSL https://download.opensuse.org/repositories/home:pzz/Debian_10/Release.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/home:pzz.gpg > /dev/null && \ + apt-get update && \ + apt-get install -yq sane sane-utils imagemagick tesseract-ocr sane-airscan && \ + sed -i 's/policy domain="coder" rights="none" pattern="PDF"/policy domain="coder" rights="read | write" pattern="PDF"'/ /etc/ImageMagick-6/policy.xml + COPY --from=builder "$APP_DIR/dist" "$APP_DIR/" -# Install dependencies RUN npm install --production -ENV NET_HOST="" +# This goes into /etc/sane.d/net.conf +ENV SANED_NET_HOSTS="" + +# This gets added to /etc/sane.d/airscan.conf +ENV AIRSCAN_DEVICES="" + +# This directs scanserv not to bother querying `scanimage -L` +ENV SCANIMAGE_LIST_IGNORE="" + +# This gets added to scanservjs/config/config.js:devices +ENV DEVICES="" + +# Override OCR language +ENV OCR_LANG="" # Copy entry point COPY run.sh /run.sh diff --git a/README.md b/README.md index 57690706..5eda725a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # scanservjs -scanservjs is a simple web-based UI for your scanner. It allows you to share a -scanner (using SANE) on a network without the need for drivers or complicated -installation. It can save to TIF, JPG, PNG and PDF with varying compression -settings, all of which can configured. It supports all +scanservjs is a simple web-based UI for your scanner. It allows you to share one +or more scanners (using SANE) on a network without the need for drivers or +complicated installation. It can save to TIF, JPG, PNG, PDF and TXT (with +Tesseract OCR) with varying compression settings, all of which can configured. +It supports multipage scanning and all [SANE compatible devices](http://www.sane-project.org/sane-supported-devices.html). ![screenshot](https://github.com/sbs20/scanservjs/raw/master/docs/screen0.png) @@ -12,6 +13,7 @@ Copyright 2016-2020 [Sam Strachan](https://github.com/sbs20) ## Requirements * SANE * ImageMagick +* Tesseract (optional) * nodejs ## Installation notes @@ -21,10 +23,14 @@ default, configuration and scanned images are stored within the container and will be lost if you recreate it. If you want to map your scanned images then specify the volume mapping option `-v /local/path/:/app/data/output/` +**Please note that the docker image is amd64 only - and will not work on ARM +devices such as the Raspberry Pi. Please follow the manual installation process +in these cases** + ```console docker pull sbs20/scanservjs:latest docker rm --force scanservjs-container 2> /dev/null -docker run -d -p 8080:8080 --restart unless-stopped --name scanservjs-container --privileged sbs20/scanservjs:latest +docker run -d -p 8080:8080 -v /var/run/dbus:/var/run/dbus --restart unless-stopped --name scanservjs-container --privileged sbs20/scanservjs:latest ``` (`--privileged` is required for the container to access the host's devices, to allow it to talk to the scanner) @@ -36,7 +42,7 @@ If you want to install the latest staging branch (this may contain newer code) ```console docker pull sbs20/scanservjs:staging docker rm --force scanservjs-container 2> /dev/null -docker run -d -p 8080:8080 --restart unless-stopped --name scanservjs-container --privileged sbs20/scanservjs:staging +docker run -d -p 8080:8080 -v /var/run/dbus:/var/run/dbus --restart unless-stopped --name scanservjs-container --privileged sbs20/scanservjs:staging ``` More installation options: @@ -45,6 +51,51 @@ More installation options: * [Development notes](docs/development.md) * [Configuring the scanner and SANE](docs/sane.md) +## Environment variables + +* `SANED_NET_HOSTS`: If you want to use a + [SaneOverNetwork](https://wiki.debian.org/SaneOverNetwork#Server_Configuration) + scanner then to perform the equivalent of adding hosts to + `/etc/sane.d/net.conf` specify a list of ip addresses separated by semicolons + in the `SANED_NET_HOSTS` environment variable. +* `AIRSCAN_DEVICES`: If you want to specifically add `sane-airscan` devices to + your `/etc/sane.d/airscan.conf` then use the `AIRSCAN_DEVICES` environment + variable (semicolon delimited). +* `DEVICES`: Force add devices use `DEVICES` (semicolon delimited) +* `SCANIMAGE_LIST_IGNORE`: To force ignore `scanimage -L` + +## Airscan +[sane-airscan](https://github.com/alexpevzner/sane-airscan) uses Avahi / +Zeroconf / Bonjour to discover devices on the local network. If you are running +docker you will want to share dbus to make it work +(`-v /var/run/dbus:/var/run/dbus`). + +## Example docker run + +### Use airscan and a locally detected scanner +This should support most use cases + +```console +docker run -d -p 8080:8080 \ + -v /var/run/dbus:/var/run/dbus \ + --name scanservjs-container --privileged scanservjs-image +``` + +### Complicated +Add two net hosts to sane, use airscan to connect to two remote scanners, don't +use `scanimage -L`, force a list of devices and override the OCR language + +```console +docker run -d -p 8080:8080 \ + -e SANED_HOSTS="10.0.100.30;10.0.100.31" \ + -e AIRSCAN_DEVICES='"Canon MFD" = "http://192.168.0.10/eSCL";"EPSON MFD" = "http://192.168.0.11/eSCL"' \ + -e SCANIMAGE_LIST_IGNORE=true \ + -e DEVICES="net:10.0.100.30:plustek:libusb:001:003;net:10.0.100.31:plustek:libusb:001:003;airscan:e0:Canon TR8500 series;airscan:e1:EPSON Cool Series" \ + -e OCR_LANG="fra" \ + -v /var/run/dbus:/var/run/dbus \ + --name scanservjs-container --privileged scanservjs-image +``` + ## Why? This is yet another scanimage-web-front-end. Why? It originally started as an adaptation of phpsane - just to make everything a bit newer, give it a refresh diff --git a/client/App.vue b/client/App.vue index 0dba84ce..a823b5d3 100644 --- a/client/App.vue +++ b/client/App.vue @@ -19,105 +19,95 @@ export default { }; - diff --git a/client/client.css b/client/client.css deleted file mode 100644 index bb9c1d6c..00000000 --- a/client/client.css +++ /dev/null @@ -1,96 +0,0 @@ -body { - padding-bottom: 20px; - font-family: 'Montserrat'; - font-size: large; - text-transform: lowercase; - background-color: white; -} - -.theme-dropdown .dropdown-menu { - position: static; - display: block; - margin-bottom: 20px; -} - -.theme-showcase > p > .btn { - margin: 5px 0; -} - -.theme-showcase .navbar .container { - width: auto; -} - -#navbar > div { - color: white; - font-size: xx-large; -} - -.container { - margin-top: 1em; -} - -.navbar-inverse { - background-color: #101030; - background-image: none; - border-color: black; -} - -.form-control { - border-radius: 0; - border: 1px solid #f0f0f0; - background-color: #f0f0f0; -} - -#image { - background-color: #d0d0e0; - background-repeat: no-repeat; - background-position: center; - border: 1px solid black; -} - -.credits { - width: 100%; - text-align: center; - margin: auto; - font-size: small; -} - -label, th { - font-size: x-large; - font-weight: 300; - color: #0E5EA0; -} - -.form-check-label { - font-size: smaller; -} - -h1 { - font-size: 4em; - font-weight: 100; - color: #0E5EA0; -} - -#mask { - position: fixed; - width: 100%; - height: 100%; - background: rgba(0,0,0,.3); - top: 0; - left: 0; - display: none; - z-index: 10; -} - -.toast-success { - background-color: green -} - -.toast-error { - background-color: red -} - -/* center the icons vertically */ -.material-icons { - vertical-align: middle; -} diff --git a/client/components/Scanserv.vue b/client/components/Scanserv.vue index 5e211426..6fb1cd8b 100644 --- a/client/components/Scanserv.vue +++ b/client/components/Scanserv.vue @@ -1,126 +1,128 @@ - + + + + Loading... + + + + - - scanserv-js (v{{ context.version }}) - - Scanner: {{ device.id }} - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ item }} - - - - - - {{ item }} - - - - - - Disabled - Enabled - - - - - - - - - - - - - - - - {{ item.description }} - - - - - - No - Yes - - - - - - - - - - - reset - clear - - - - preview - scan - - - - - - - - - - + + + + + + scanserv-js (v{{ context.version }}) + + + + {{ item.id }} + + + + + + {{ item }} + + + + + + {{ item }} + + + + + + {{ item }} + + + + + + Disabled + Enabled + + + + + + None + Manual (with prompt) + Automatic (use ADF) + + + + + + {{ item.description }} + + + + + + + reset + preview + scan + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - File - Date + Filename + Date Size @@ -128,14 +130,14 @@ {{ file.name }} - {{ file.lastModified }} + {{ file.lastModified }} {{ file.sizeString }} - + - - + + @@ -156,7 +158,7 @@ export default { data() { const device = { - name: 'Unspecified', + id: 'Unspecified', features: { '--mode': { options: [], @@ -215,18 +217,27 @@ export default { files: [], img: null, maskRef: 0, - request: request + request: request, + preview: { + timer: 0, + width: 400, + key: 0 + } }; }, mounted() { this.$refs.toastr.defaultPosition = 'toast-bottom-right'; this.$refs.toastr.defaultTimeout = 5000; - + this._updatePreview(); this.readContext().then(() => { this.readPreview(); }); this.fileList(); + window.addEventListener('resize', () => { + clearTimeout(this.preview.timer); + this.preview.timer = setTimeout(this._updatePreview, 100); + }); }, watch: { @@ -241,6 +252,16 @@ export default { }, methods: { + _updatePreview() { + const isoPaperRatio = 215 / 297; + if (window.innerWidth < 576) { + this.preview.width = window.innerWidth - (window.scrollbars.visible ? 25 : 0) - 30; + } else { + this.preview.width = (window.innerHeight - 120) * isoPaperRatio; + } + this.preview.key += 1; + }, + _clone(o) { return JSON.parse(JSON.stringify(o)); }, @@ -276,6 +297,10 @@ export default { } }).then(() => { window.clearInterval(timer); + + // Some scanners don't create the preview until after the scan has finished. + // Run preview one last time + window.setTimeout(this.readPreview, 1000); this.mask(-1); }); }, @@ -356,13 +381,21 @@ export default { return this._fetch(url).then(context => { this.context = context; - this.device = context.devices[0]; - this.$refs.toastr.i(`Found device ${this.device.id}`); - this.request = this.readRequest(); - for (let test of context.diagnostics) { - const toast = test.success ? this.$refs.toastr.s : this.$refs.toastr.e; - toast(test.message); + + if (context.devices.length > 0) { + for (let device of context.devices) { + this.$refs.toastr.i(`Found device ${device.id}`); + } + this.device = context.devices[0]; + this.request = this.readRequest(); + for (let test of context.diagnostics) { + const toast = test.success ? this.$refs.toastr.s : this.$refs.toastr.e; + toast(test.message); + } + } else { + this.$refs.toastr.e('Found no devices'); } + if (force) { this.clear(); this.readPreview(); @@ -386,7 +419,6 @@ export default { }, readRequest() { - const device = this.device; let request = null; if (localStorage.request) { request = JSON.parse(localStorage.request); @@ -395,7 +427,13 @@ export default { } console.log('load', request); } - + + if (request !== null) { + this.device = this.context.devices.filter(d => d.id === request.params.deviceId)[0] + || this.context.devices[0]; + } + const device = this.device; + if (request === null) { request = { version: this.context.version, @@ -406,23 +444,25 @@ export default { width: device.features['-x'].limits[1], height: device.features['-y'].limits[1], resolution: device.features['--resolution'].default, - mode: device.features['--mode'].default, - brightness: 0, - contrast: 0, - dynamicLineart: true + mode: device.features['--mode'].default }, - pipeline: this.context.pipelines[0].description + pipeline: this.context.pipelines[0].description, + batch: 'none', + page: 1 }; } - if ('--brightness' in device.features === false) { - delete request.params.brightness; + if ('--source' in device.features) { + request.params.source = device.features['--source'].default; + } + if ('--brightness' in device.features) { + request.params.brightness = 0; } - if ('--contrast' in device.features === false) { - delete request.params.contrast; + if ('--contrast' in device.features) { + request.params.contrast = 0; } - if ('--disable-dynamic-lineart' in device.features === false) { - delete request.params.dynamicLineart; + if ('--disable-dynamic-lineart' in device.features) { + request.params.dynamicLineart = true; } return request; @@ -451,7 +491,7 @@ export default { } }).then((data) => { if (data && 'page' in data) { - if (window.confirm('More?')) { + if (window.confirm(`Scan page ${data.page}?`)) { this.request.page = data.page; this.scan(); } else { @@ -461,8 +501,8 @@ export default { } else { this.request.page = 1; this.fileList(); - this.mask(-1); } + this.mask(-1); }); } } @@ -471,13 +511,12 @@ export default { \ No newline at end of file diff --git a/config/config.js b/config/config.js index effa330c..1d7a1747 100644 --- a/config/config.js +++ b/config/config.js @@ -1,106 +1,174 @@ const dayjs = require('dayjs'); -const Config = { - port: 8080, +const Config = {}; - scanimage: '/usr/bin/scanimage', - convert: '/usr/bin/convert', - tesseract: '', +// Things to change +Config.port = 8080; +Config.devices = []; +Config.ocrLanguage = 'eng'; +Config.log = {}; +Config.log.level = 'DEBUG'; +Config.filename = () => { + return `scan_${dayjs().format('YYYY-MM-DD HH.mm.ss')}`; +}; - ocrLanguage: 'eng', - outputDirectory: './data/output/', - previewDirectory: './data/preview/', - previewResolution: 100, - - log: { - level: 'DEBUG', - prefix: { - template: '[%t] %l (%n):', - levelFormatter(level) { - return level.toUpperCase(); - }, - nameFormatter(name) { - return name || 'global'; - }, - timestampFormatter(date) { - return date.toISOString(); - }, - } +// Probably do not change +Config.scanimage = '/usr/bin/scanimage'; +Config.convert = '/usr/bin/convert'; +Config.tesseract = '/usr/bin/tesseract'; +Config.log.prefix = { + template: '[%t] %l (%n):', + levelFormatter(level) { + return level.toUpperCase(); }, - filename() { - return `scan_${dayjs().format('YYYY-MM-DD HH.mm.ss')}`; + nameFormatter(name) { + return name || 'global'; }, - previewPipeline: { - extension: 'jpg', - description: 'JPG (Low quality)', - commands: [ - 'convert - -quality 75 jpg:-' - ] - } -} + timestampFormatter(date) { + return date.toISOString(); + }, +}; +// No need to change +Config.outputDirectory = './data/output/'; +Config.previewDirectory = './data/preview/'; +Config.tempDirectory = './data/temp/'; +Config.previewResolution = 100; +Config.previewPipeline = { + extension: 'jpg', + description: 'JPG (Low quality)', + commands: [ + 'convert - -quality 75 jpg:-' + ] +}; + +/* When all scans are complete, the filenames are all piped into stdin of the +first pipeline command. It would be nicer to pipe the binary output of scanimage +but that doesn't work with multipage scans so we have no choice but to write to +the filesystem. + +The stdout of each pipeline feeds into the stdin of the next. Although clumsy in +some respects (especially where we have to write temporary files and then list +them) it at least provides a means of user configuration with "just" shell +scripting. + +The overall output of the pipelines (i.e. the last pipeline output) must be a +list of the files you want kept. The convention is to output files of the form +`scan-0000.ext` but it's convention only. You can output whatever you want. If +multiple files are output then the results will be zipped into a single file. + +Each command is executed with the CWD set to the temporary location so no +directory traversal is required. Pipeline commands are always read from this +file (and never from the browser request, even though it is sent). It would be +possible to subvert these commands for malicious use, but it doesn't give any +further privilege than the user account running scanservjs and still requires +access to this file. You obviously should not be running as root. + +Some useful pointers: +- `convert` can read a list of files from a file with the @ argument. The `-` + file is stdin. So `convert @- -argument output` performs the conversion on + each file piped into stdin +- `tesseract` has a similar feature using `-c stream_filelist=true` +- `convert` can also output multiple files if you use an output filename with + `%d` in it. C string style formatting is available so you can do things like + output to `scan-%04d.jpg`. Formats which do not support multiple pages must + use this option. Multi-page formats including PDF and TIF do not use this + option. +- if you just wanted to take a filename from stdin and have its content read out + you could `xargs cat` provided there were no spaces or commas in the filename + (which there won't be) +*/ Config.pipelines = [ { extension: 'jpg', - description: 'JPG (High quality)', + description: 'JPG | High quality', commands: [ - 'convert - -quality 92 jpg:-' + 'convert @- -quality 92 scan-%04d.jpg', + 'ls scan-*.*' ] }, { extension: 'jpg', - description: 'JPG (Medium quality)', + description: 'JPG | Medium quality', commands: [ - 'convert - -quality 75 jpg:-' + 'convert @- -quality 75 scan-%04d.jpg', + 'ls scan-*.*' ] }, { extension: 'jpg', - description: 'JPG (Low quality)', + description: 'JPG | Low quality', commands: [ - 'convert - -quality 50 jpg:-' + 'convert @- -quality 50 scan-%04d.jpg', + 'ls scan-*.*' ] }, { extension: 'png', description: 'PNG', commands: [ - 'convert - -quality 75 png:-' + 'convert @- -quality 75 scan-%04d.png', + 'ls scan-*.*' ] }, { extension: 'tif', - description: 'TIF (Uncompressed)', - commands: [] + description: 'TIF | Uncompressed', + commands: [ + 'convert @- scan-0000.tif', + 'ls scan-*.*' + ] }, { extension: 'tif', - description: 'TIF (LZW)', + description: 'TIF | LZW compression', commands: [ - 'convert - -compress lzw tif:-' + 'convert @- -compress lzw scan-0000.tif', + 'ls scan-*.*' ] }, { extension: 'pdf', - description: 'PDF (TIF)', + description: 'PDF (TIF | Uncompressed)', commands: [ - 'convert - pdf:-' + 'convert @- scan-0000.pdf', + 'ls scan-*.*' ] }, { extension: 'pdf', - description: 'PDF (LZW TIF)', + description: 'PDF (TIF | LZW Compression)', commands: [ - 'convert - -compress lzw tif:-', - 'convert - pdf:-' + 'convert @- -compress lzw tmp-%04d.tif && ls tmp-*.tif', + 'convert @- scan-0000.pdf', + 'ls scan-*.*' ] }, { extension: 'pdf', - description: 'PDF (JPG)', + description: 'PDF (JPG | High quality)', commands: [ - 'convert - -quality 92 jpg:-', - 'convert - pdf:-' + 'convert @- -quality 92 tmp-%04d.jpg && ls tmp-*.jpg', + 'convert @- scan-0000.pdf', + 'ls scan-*.*' + ] + }, + { + extension: 'pdf', + description: 'PDF (JPG | Medium quality)', + commands: [ + 'convert @- -quality 75 tmp-%04d.jpg && ls tmp-*.jpg', + 'convert @- scan-0000.pdf', + 'ls scan-*.*' + ] + }, + { + extension: 'pdf', + description: 'PDF (JPG | Low quality)', + commands: [ + 'convert @- -quality 50 tmp-%04d.jpg && ls tmp-*.jpg', + 'convert @- scan-0000.pdf', + 'ls scan-*.*' ] } ]; @@ -109,22 +177,45 @@ if (Config.tesseract) { Config.pipelines = Config.pipelines.concat([ { extension: 'pdf', - description: 'PDF (JPG) with OCR text', + description: 'OCR | PDF (JPG | High quality)', commands: [ - 'cat > tmp.tif && ls tmp.tif', 'convert @- -quality 92 tmp-%d.jpg && ls tmp-*.jpg', - `${Config.tesseract} -l ${Config.ocrLanguage} -c stream_filelist=true - - pdf && rm -f tmp-*.jpg` + `${Config.tesseract} -l ${Config.ocrLanguage} -c stream_filelist=true - - pdf > scan-0001.pdf`, + 'ls scan-*.*' ] }, { extension: 'txt', - description: 'Text file (OCR)', + description: 'OCR | Text file', commands: [ - 'cat > tmp.tif && ls tmp.tif', - `${Config.tesseract} -l ${Config.ocrLanguage} -c stream_filelist=true - - txt && rm -f tmp-*.tif` + `${Config.tesseract} -l ${Config.ocrLanguage} -c stream_filelist=true - - txt > scan-0001.txt`, + 'ls scan-*.*' ] } ]); } +// Process environment variables + +// scanservjs will attempt to find scanners locally using `scanimage -L` but +// sometimes you may need to manually add network devices here if they're not +// found e.g. +// Config.devices = ['net:192.168.0.10:airscan:e0:Canon TR8500 series']; +// This is done with an environment variable. Multiple entries are separated by +// semicolons +if (process.env.DEVICES !== undefined && process.env.DEVICES.length > 0) { + Config.devices = process.env.DEVICES.split(';'); +} + +// scanservjs will attempt to find scanners locally using `scanimage -L` but +// sometimes it will return nothing. If you are specifying devices manually you +// may also with to turn off the find. +Config.devicesFind = process.env.SCANIMAGE_LIST_IGNORE === undefined + || process.env.SCANIMAGE_LIST_IGNORE.length === 0; + +// Override the OCR language here +if (process.env.OCR_LANG !== undefined && process.env.OCR_LANG.length > 0) { + Config.ocrLanguage = process.env.OCR_LANG; +} + module.exports = Config; diff --git a/data/output/empty.md b/data/output/empty.md deleted file mode 100644 index 82abf17d..00000000 --- a/data/output/empty.md +++ /dev/null @@ -1,3 +0,0 @@ -The /output directory is required for the scanner output. If you use xcopy to -deploy then empty directories are not copied by default. This file is only to -make sure the directory is not empty. \ No newline at end of file diff --git a/docs/development.md b/docs/development.md index a7e49d91..5974596b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -54,6 +54,10 @@ gulp release Install docker ``` sudo apt install docker.io +sudo systemctl unmask docker +sudo systemctl start docker + +# Hack to make docker accessible. sudo chmod 666 /var/run/docker.sock ``` @@ -62,7 +66,11 @@ Useful commands # Build and run docker build -t scanservjs-image . docker rm --force scanservjs-container 2> /dev/null -docker run -d -p 8080:8080 --name scanservjs-container --privileged scanservjs-image +docker run -d -p 8080:8080 -v /var/run/dbus:/var/run/dbus --name scanservjs-container --privileged scanservjs-image + +# Copy image +docker save -o scanservjs-image.tar scanservjs-image +docker load -i scanservjs-image.tar # Debug docker run -it --entrypoint=/bin/bash scanservjs-container diff --git a/docs/install.md b/docs/install.md index c318e22b..fcfb4e6c 100644 --- a/docs/install.md +++ b/docs/install.md @@ -5,13 +5,15 @@ manually. ## Steps * Get [SANE installed and working](./sane.md) and check permissions etc. -* Get nodejs and npm installed (e.g. or +* Get nodejs and npm installed (You will need an up to date version of npm, you + may need to run `npm install npm@latest -g`) * Download the latest release of scanserv, extract it and run `install.sh` ## tl;dr; (Debian 10) ``` sudo apt install -y nodejs npm sane-utils imagemagick curl +sudo npm install npm@latest -g wget -O ~/scanservjs.tar.gz $(curl -s https://api.github.com/repos/sbs20/scanservjs/releases/latest | grep browser_download_url | cut -d '"' -f 4) mkdir scanservjs tar -xf scanservjs.tar.gz -C ./scanservjs/ diff --git a/docs/sane.md b/docs/sane.md index 9f5855d4..62caac5c 100644 --- a/docs/sane.md +++ b/docs/sane.md @@ -35,6 +35,60 @@ underlying problem is. In order of likelihood: Unplug / replug the scanner. * Add current user to the `scanner` group +## SANE Airscan + +You may find [sane-airscan](https://github.com/alexpevzner/sane-airscan) useful +for supporting newer eSCL and WSD devices as the standard sane-escl package +doesn't seem to be widely available in most package managers yet. Once installed +you should just find that it works with a simple `scanimage -L`. You can also +specify a specific name for the device in `/etc/sane.d/airscan.conf` + +```console +[devices] +"My scanner" = "http://10.0.111.4/eSCL" +``` +Your URI may be different + +Airscan relies on bonjour to make it work. If you are running on a different +subnet, you can use saned to share it in the normal way and add the IP address +of the saned server to `/etc/sane.d/net.conf` on the client (which will be the +scanservjs server). + +## Defining network scanners + +From the scanimage manpage: + +> The -L or --list-devices option requests a (partial) list of devices that are +> available. The list is not complete since some devices may be available, but +> are not listed in any of the configuration files (which are typically stored +> in directory /etc/sane.d). This is particularly the case when accessing +> scanners through the network. If a device is not listed in a configuration +> file, the only way to access it is by its full device name. You may need to +> consult your system administrator to find out the names of such devices. + +Find the name of the scanner on the remote system using `scanimage -L` e.g: + +``` +device `airscan:e0:Canon TR8500 series' is a eSCL Canon TR8500 series eSCL network scanner +``` + +Then on the client, prefix it with `net::` so it becomes: + +``` +net:192.168.0.10:airscan:e0:Canon TR8500 series' +``` + +For more information on configuring the server and client see +[SaneOverNetwork](https://wiki.debian.org/SaneOverNetwork#Server_Configuration). + +TL;DR; for configuring server: + +```console +# Allow access from network +echo "192.168.0.1/24" >> /etc/sane.d/saned.conf +sudo systemctl enable saned.socket +sudo systemctl start saned.socket +``` ## For QNAP NAS ### install [Works on QTS 4.2.2] diff --git a/docs/screen0.png b/docs/screen0.png index 37182219..dc6d6086 100644 Binary files a/docs/screen0.png and b/docs/screen0.png differ diff --git a/gulpfile.js b/gulpfile.js index 22976a6a..da7caa37 100755 --- a/gulpfile.js +++ b/gulpfile.js @@ -55,18 +55,15 @@ gulp.task('clean', () => { return del(['./dist/*']); }); -gulp.task('client-build', () => { - return run('npm run client-build').exec(); -}); - gulp.task('server-lint', () => { - return gulp.src(['./server/*.js', './test/**/*.js', 'gulpfile.js']) + return gulp.src(['./server/*.js', './config/config.js', './test/**/*.js', 'gulpfile.js']) .pipe(linter()) .pipe(eslint.format()) .pipe(eslint.failAfterError()); }); gulp.task('server-build', () => { + const shellFilter = filter('**/*.sh', {restore: true}); return gulp.src([ './install.sh', './uninstall.sh', @@ -76,19 +73,14 @@ gulp.task('server-build', () => { './*config/**/*.js', './*server/**/*', './*data/**/*.md', - './*data/**/default.jpg', - ]).pipe(gulp.dest('./dist/')); -}); - -gulp.task('test', () => { - return run('npm run test').exec(); + './*data/**/default.jpg']) + .pipe(shellFilter) + .pipe(chmod(0o755)) + .pipe(shellFilter.restore) + .pipe(gulp.dest('./dist/')); }); -gulp.task('build', gulp.series(['clean', 'server-lint', 'client-build', 'server-build', 'test'], (done) => { - done(); -})); - -gulp.task('release', gulp.series(['build'], () => { +gulp.task('package', () => { const filename = `scanservjs_v${version}_${dayjs().format('YYYYMMDD.HHmmss')}.tar`; const shellFilter = filter('**/*.sh', {restore: true}); return gulp.src('./dist/**/*') @@ -101,6 +93,27 @@ gulp.task('release', gulp.series(['build'], () => { .pipe(tar(filename)) .pipe(gzip()) .pipe(gulp.dest('./release')); +}); + +/* +Development helpers below. These tasks rely on running a command line which is +not available in all circumstances. +*/ + +gulp.task('client-build', () => { + return run('npm run client-build').exec(); +}); + +gulp.task('test', () => { + return run('npm run test').exec(); +}); + +gulp.task('build', gulp.series(['clean', 'server-lint', 'client-build', 'server-build', 'test'], (done) => { + done(); +})); + +gulp.task('release', gulp.series(['build', 'package'], (done) => { + done(); })); gulp.task('default', gulp.series(['build'], (done) => { diff --git a/install.sh b/install.sh index 7699d052..d5e59cbe 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,13 @@ if ! [ -x "$(command -v npm)" ]; then exit 1 fi +# You may wish to install additional components such as airscan and tesseract. +# apt-get install -yq curl gpg +# echo 'deb http://download.opensuse.org/repositories/home:/pzz/Debian_10/ /' | tee /etc/apt/sources.list.d/home:pzz.list +# curl -fsSL https://download.opensuse.org/repositories/home:pzz/Debian_10/Release.key | gpg --dearmor | tee /etc/apt/trusted.gpg.d/home:pzz.gpg > /dev/null +# apt-get update +# apt-get install -yq sane sane-utils imagemagick tesseract-ocr sane-airscan + # Set correct src dir srcdir="$( cd "$( dirname "$0" )" && pwd )" @@ -79,7 +86,8 @@ cp scanservjs.service /etc/systemd/system # Reload the deamon info systemctl daemon-reload -# Start the new service +# Enable and start the new service +systemctl enable scanservjs systemctl start scanservjs echo "scanservjs starting" diff --git a/package-lock.json b/package-lock.json index 1c010e4c..10f9b287 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "scanservjs", - "version": "2.0.3", + "version": "2.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2290,6 +2290,11 @@ "integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==", "dev": true }, + "adm-zip": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", + "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==" + }, "aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -2330,6 +2335,12 @@ "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -2535,6 +2546,16 @@ "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", "dev": true }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "dev": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -2592,6 +2613,12 @@ "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", "dev": true }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -2818,6 +2845,12 @@ "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", "dev": true }, + "async-foreach": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz", + "integrity": "sha1-NhIfhFwFeBct5Bmpfb6x0W7DRUI=", + "dev": true + }, "async-limiter": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", @@ -3047,6 +3080,15 @@ "safe-buffer": "^5.1.1" } }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "~2.0.0" + } + }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -3661,6 +3703,24 @@ "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", "dev": true }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "dev": true, + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", + "dev": true + } + } + }, "caniuse-api": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", @@ -4263,6 +4323,12 @@ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", "dev": true }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "consolidate": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", @@ -4802,6 +4868,15 @@ } } }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "requires": { + "array-find-index": "^1.0.1" + } + }, "cyclist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", @@ -5282,6 +5357,12 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true + }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -6718,6 +6799,18 @@ "dev": true, "optional": true }, + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -6730,6 +6823,68 @@ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + } + } + }, + "gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "requires": { + "globule": "^1.0.0" + } + }, "gensync": { "version": "1.0.0-beta.1", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", @@ -6742,6 +6897,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -6901,7 +7062,10 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -7001,6 +7165,17 @@ "slash": "^2.0.0" } }, + "globule": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz", + "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==", + "dev": true, + "requires": { + "glob": "~7.1.1", + "lodash": "~4.17.10", + "minimatch": "~3.0.2" + } + }, "glogg": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.2.tgz", @@ -7630,6 +7805,12 @@ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -8086,6 +8267,12 @@ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, + "in-publish": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz", + "integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ==", + "dev": true + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -8453,6 +8640,12 @@ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, + "is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true + }, "is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -8753,6 +8946,12 @@ } } }, + "js-base64": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", + "integrity": "sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ==", + "dev": true + }, "js-message": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.5.tgz", @@ -8901,6 +9100,12 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", + "dev": true + }, "last-run": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", @@ -9331,6 +9536,16 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, "lower-case": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", @@ -9377,6 +9592,12 @@ "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", "dev": true }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", + "dev": true + }, "map-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", @@ -9453,6 +9674,24 @@ "readable-stream": "^2.0.1" } }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "dev": true, + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -10146,6 +10385,12 @@ "thenify-all": "^1.0.0" } }, + "nan": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", + "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==", + "dev": true + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -10215,6 +10460,43 @@ "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "dev": true }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "dev": true, + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } + } + }, "node-ipc": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/node-ipc/-/node-ipc-9.1.1.tgz", @@ -10271,6 +10553,99 @@ "integrity": "sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g==", "dev": true }, + "node-sass": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.14.1.tgz", + "integrity": "sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g==", + "dev": true, + "requires": { + "async-foreach": "^0.1.3", + "chalk": "^1.1.1", + "cross-spawn": "^3.0.0", + "gaze": "^1.0.0", + "get-stdin": "^4.0.1", + "glob": "^7.0.3", + "in-publish": "^2.0.0", + "lodash": "^4.17.15", + "meow": "^3.7.0", + "mkdirp": "^0.5.1", + "nan": "^2.13.2", + "node-gyp": "^3.8.0", + "npmlog": "^4.0.0", + "request": "^2.88.0", + "sass-graph": "2.2.5", + "stdout-stream": "^1.4.0", + "true-case-path": "^1.0.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cross-spawn": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-3.0.1.tgz", + "integrity": "sha1-ElYDfsufDF9549bvE14wdwGEuYI=", + "dev": true, + "requires": { + "lru-cache": "^4.0.1", + "which": "^1.2.9" + } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, "nodemon": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.4.tgz", @@ -10363,6 +10738,18 @@ "path-key": "^2.0.0" } }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, "nth-check": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", @@ -10727,6 +11114,12 @@ "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", "dev": true }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, "os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -10742,6 +11135,16 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", @@ -12123,6 +12526,36 @@ "resolve": "^1.1.6" } }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "dev": true, + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "dependencies": { + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "dev": true, + "requires": { + "repeating": "^2.0.0" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "dev": true, + "requires": { + "get-stdin": "^4.0.1" + } + } + } + }, "regenerate": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.1.tgz", @@ -12358,6 +12791,15 @@ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "dev": true, + "requires": { + "is-finite": "^1.0.0" + } + }, "replace-ext": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", @@ -12595,6 +13037,140 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sass-graph": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.2.5.tgz", + "integrity": "sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag==", + "dev": true, + "requires": { + "glob": "^7.0.0", + "lodash": "^4.0.0", + "scss-tokenizer": "^0.2.3", + "yargs": "^13.3.2" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "cliui": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", + "dev": true, + "requires": { + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" + } + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dev": true, + "requires": { + "ansi-regex": "^4.1.0" + } + }, + "wrap-ansi": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + } + }, + "yargs": { + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", + "dev": true, + "requires": { + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" + } + }, + "yargs-parser": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", + "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "sass-loader": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.2.tgz", + "integrity": "sha512-wV6NDUVB8/iEYMalV/+139+vl2LaRFlZGEd5/xmdcdzQcgmis+npyco6NsDTVOlNA3y2NV9Gcz+vHyFMIT+ffg==", + "dev": true, + "requires": { + "klona": "^2.0.3", + "loader-utils": "^2.0.0", + "neo-async": "^2.6.2", + "schema-utils": "^2.7.1", + "semver": "^7.3.2" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", + "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", + "dev": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + } + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -12612,6 +13188,27 @@ "ajv-keywords": "^3.5.2" } }, + "scss-tokenizer": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", + "integrity": "sha1-jrBtualyMzOCTT9VMGQRSYR85dE=", + "dev": true, + "requires": { + "js-base64": "^2.1.8", + "source-map": "^0.4.2" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -13277,6 +13874,15 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, + "stdout-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", + "dev": true, + "requires": { + "readable-stream": "^2.0.1" + } + }, "stream-browserify": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", @@ -13600,6 +14206,17 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "dev": true, + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + } + }, "tar-stream": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", @@ -13888,6 +14505,21 @@ "punycode": "^2.1.1" } }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "dev": true + }, + "true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dev": true, + "requires": { + "glob": "^7.1.2" + } + }, "tryer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", @@ -14890,7 +15522,10 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", @@ -15224,7 +15859,10 @@ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", "dev": true, - "optional": true + "optional": true, + "requires": { + "nan": "^2.12.1" + } }, "glob-parent": { "version": "3.1.0", diff --git a/package.json b/package.json index 9a20c73c..642c26e1 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "scanservjs", - "version": "2.0.3", + "version": "2.1.0", "description": "scanservjs is a simple web-based UI for SANE which allows you to share a scanner on a network without the need for drivers or complicated installation. scanserv does not do image conversion or manipulation (beyond the bare minimum necessary for the purposes of browser preview) or OCR.", "scripts": { "serve": "nodemon --exec 'vue-cli-service serve'", + "server-lint": "gulp server-lint", "server-build": "gulp server-build", "client-build": "vue-cli-service build", - "lint": "vue-cli-service lint", + "client-lint": "vue-cli-service lint", + "package": "gulp package", "test": "mocha" }, "main": "server/server.js", @@ -34,6 +36,8 @@ "gulp-tar": "^2.1.0", "mocha": "8.1.3", "nodemon": "^2.0.4", + "node-sass": "4.14.1", + "sass-loader": "10.0.2", "vue-advanced-cropper": "0.16.10", "vue-slider-component": "3.2.5", "vue-template-compiler": "^2.6.11", @@ -42,6 +46,7 @@ "author": "Sam Strachan", "license": "GPL-2.0", "dependencies": { + "adm-zip": "0.4.16", "body-parser": "^1.19.0", "core-js": "^3.6.5", "dayjs": "1.8.35", diff --git a/run.sh b/run.sh index cef28ccd..8e973bd9 100644 --- a/run.sh +++ b/run.sh @@ -1,5 +1,31 @@ #!/bin/sh set -xve -[ ! -z "$NET_HOST" ] && echo $NET_HOST > /etc/sane.d/net.conf + +# turn off globbing +set -f + +# split at newlines only (airscan devices can have spaces in) +IFS=' +' + +# Insert a list of net hosts +if [ ! -z "$SANED_NET_HOSTS" ]; then + hosts=$(echo $SANED_NET_HOSTS | sed "s/;/\n/") + for host in $hosts; do + echo $host >> /etc/sane.d/net.conf + done +fi + +# Insert airscan devices +if [ ! -z "$AIRSCAN_DEVICES" ]; then + devices=$(echo $AIRSCAN_DEVICES | sed "s/;/\n/") + for device in $devices; do + sed -i "/^\[devices\]/a $device" /etc/sane.d/airscan.conf + done +fi + +unset IFS +set +f + node ./server/server.js diff --git a/scanservjs.service b/scanservjs.service index 77c33135..142a040f 100755 --- a/scanservjs.service +++ b/scanservjs.service @@ -7,7 +7,7 @@ ExecStart=/var/www/scanservjs/server/server.js Restart=always User=scanservjs Group=users -Environment=PATH=/usr/bin:/usr/local/bin +Environment=PATH=/usr/bin:/usr/local/bin:/bin Environment=NODE_ENV=production WorkingDirectory=/var/www/scanservjs diff --git a/server/api.js b/server/api.js index 2be63575..759b7de8 100644 --- a/server/api.js +++ b/server/api.js @@ -1,33 +1,24 @@ -const fs = require('fs'); const log = require('loglevel').getLogger('Api'); - const Config = require('../config/config'); +const Constants = require('./constants'); const Context = require('./context'); -const Device = require('./device'); +const Devices = require('./devices'); const FileInfo = require('./file-info'); const Process = require('./process'); const Request = require('./request'); const Scanimage = require('./scanimage'); +const util = require('./util'); class Api { static async fileList() { log.debug('fileList()'); - return await new Promise((resolve, reject) => { - let outdir = Config.outputDirectory; - fs.readdir(outdir, (err, list) => { - if (err) { - reject(err); - } - - let files = list - .map(f => new FileInfo(outdir + f)) - .filter(f => ['.tif', '.jpg', '.png', '.pdf', '.txt'].includes(f.extension)) - .sort((f1, f2) => f2.lastModified - f1.lastModified); - - log.debug(JSON.stringify(files)); - resolve(files); - }); - }); + const dir = new FileInfo(Config.outputDirectory); + let files = await dir.list(); + files = files + .filter(f => ['.tif', '.jpg', '.png', '.pdf', '.txt', '.zip'].includes(f.extension)) + .sort((f1, f2) => f2.lastModified - f1.lastModified); + log.debug(JSON.stringify(files)); + return files; } static fileDelete(fullpath) { @@ -65,7 +56,7 @@ class Api { const source = new FileInfo(`${Config.previewDirectory}preview.tif`); if (source.exists()) { const buffer = source.toBuffer(); - return await Process.chain(Config.previewPipeline.commands, buffer, true); + return await Process.chain(Config.previewPipeline.commands, buffer, { ignoreErrors: true }); } // If not then it's possible the default image is not quite the correct aspect ratio @@ -83,49 +74,69 @@ class Api { static async scan(req) { const context = await Context.create(); const request = new Request(context).extend(req); - const stem = '~tmp-scan-'; + const dir = FileInfo.create(Config.tempDirectory); + + // Check pipeline here. Better to find out sooner if there's a problem + const pipeline = context.pipelines.filter(p => p.description === request.pipeline)[0]; + if (pipeline === undefined) { + throw Error('No matching pipeline'); + } + + const clearTemp = async () => { + const files = await dir.list(); + files.map(f => f.delete()); + }; + + if (request.page === 1) { + log.debug('Clearing temp directory'); + await clearTemp(); + } - if (request.batch === undefined || request.batch === false) { - const pipeline = context.pipelines.filter(p => p.description === request.pipeline)[0]; - if (pipeline === undefined) { - throw Error('No matching pipeline'); + if (request.page > 0) { + log.debug('Scanning'); + await Process.spawn(Scanimage.scan(request)); + } + + if (request.batch !== Constants.BATCH_MANUAL || request.page < 1) { + log.debug(`Post processing: ${pipeline.description}`); + const files = await dir.list(); + const stdin = files + .filter(f => f.extension === '.tif') + .map(f => f.name) + .join('\n'); + log.debug('Executing cmds:', pipeline.commands); + const stdout = await Process.chain(pipeline.commands, stdin, { cwd: Config.tempDirectory }); + let filenames = stdout.toString().split('\n').filter(f => f.length > 0); + + let filename = filenames[0]; + let extension = pipeline.extension; + if (filenames.length > 1) { + filename = 'archive.zip'; + extension = 'zip'; + util.zip( + filenames.map(f => `${Config.tempDirectory}${f}`), + `${Config.tempDirectory}${filename}`); } - const cmds = [Scanimage.scan(request)].concat(pipeline.commands); - log.debug('Executing cmds:', cmds); - const buffer = await Process.chain(cmds); - const filename = `${Config.outputDirectory}${Config.filename()}.${pipeline.extension}`; - const file = new FileInfo(filename); - file.save(buffer); - log.debug(`Written data to: ${filename}`); - return {}; - } else if (request.batch && request.page > 0) { - const buffer = await Process.spawn(Scanimage.scan(request)); - const number = `000${request.page}`.slice(-4); - const filename = `${stem}${number}.tif`; - const file = new FileInfo(filename); - file.save(buffer); - log.debug(`Written data to: ${filename}`); - return { - page: request.page + 1 - }; - - } else { - const pipeline = context.pipelines.filter(p => p.description === request.pipeline)[0]; - const cmds = [`ls ${stem}*.tif`].concat(pipeline.commands); - log.debug('Executing cmds:', cmds); - const buffer = await Process.chain(cmds); - const filename = `${Config.outputDirectory}${Config.filename()}.${pipeline.extension}`; - const file = new FileInfo(filename); - file.save(buffer); - log.debug(`Written data to: ${filename}`); + const destination = `${Config.outputDirectory}${Config.filename()}.${extension}`; + await FileInfo + .create(`${Config.tempDirectory}${filename}`) + .move(destination); + + log.debug(`Written data to: ${destination}`); + await clearTemp(); return {}; } + + log.debug(`Scan page: ${request.page + 1}?`); + return { + page: request.page + 1 + }; } static async context(force) { if (force) { - Device.reset(); + Devices.reset(); const preview = new FileInfo(`${Config.previewDirectory}preview.tif`); preview.delete(); } diff --git a/server/configure.js b/server/configure.js index 4ec11dad..c52417d6 100644 --- a/server/configure.js +++ b/server/configure.js @@ -1,3 +1,4 @@ +const fs = require('fs'); const rootLog = require('loglevel'); const prefix = require('loglevel-plugin-prefix'); const Config = require('../config/config'); @@ -41,7 +42,13 @@ const logRequest = (req) => { log.debug('request: ', output); }; +const initialize = () => { + fs.mkdirSync(Config.outputDirectory, { recursive: true }); + fs.mkdirSync(Config.tempDirectory, { recursive: true }); +}; + module.exports = app => { + initialize(); app.use(bodyParser.urlencoded({ extended: true })); diff --git a/server/constants.js b/server/constants.js new file mode 100644 index 00000000..a371b7d9 --- /dev/null +++ b/server/constants.js @@ -0,0 +1,6 @@ +module.exports = { + BATCH_NONE: 'none', + BATCH_MANUAL: 'manual', + BATCH_AUTO: 'auto', + TEMP_FILESTEM: '~tmp-scan-' +}; \ No newline at end of file diff --git a/server/context.js b/server/context.js index b63b1550..0a47e7e8 100644 --- a/server/context.js +++ b/server/context.js @@ -1,6 +1,6 @@ const fs = require('fs'); const Config = require('../config/config'); -const Device = require('./device'); +const Devices = require('./devices'); const Package = require('../package.json'); const diagnostic = (path) => { @@ -24,7 +24,7 @@ class Context { } static async create() { - const devices = [await Device.get()]; + const devices = await Devices.get(); return new Context(devices); } diff --git a/server/device.js b/server/device.js index d4ad7311..f03205fb 100644 --- a/server/device.js +++ b/server/device.js @@ -1,13 +1,5 @@ -const log = require('loglevel').getLogger('Device'); - const extend = require('./util').extend; -const FileInfo = require('./file-info'); const Package = require('../package.json'); -const Process = require('./process'); -const Scanimage = require('./scanimage'); - -// Relative to execution path -const FILEPATH = './config/devices.json'; class Feature { static splitNumbers(string, delimiter) { @@ -57,6 +49,7 @@ class Adapter { const feature = device.features[key]; switch (key) { case '--mode': + case '--source': feature.options = feature.parameters.split('|'); break; @@ -140,37 +133,6 @@ class Device { throw new Error('Unexpected data for Device'); } } - - /// Attempts to get a stored configuration of our device and if - /// not gets it from the command line. - static async get() { - const file = new FileInfo(FILEPATH); - let isCached = true; - if (!file.exists()) { - log.debug('device.conf does not exist. Reloading'); - isCached = false; - } else if (Device.from(file.toJson()).version !== Package.version) { - log.debug('device.conf version is old. Reloading'); - isCached = false; - } - - if (!isCached) { - const data = await Process.execute(Scanimage.all()); - log.debug(data); - const device = Device.from(data); - file.save(JSON.stringify(device, null, 2)); - return device; - } else { - return Device.from(file.toJson()); - } - } - - static reset() { - const file = new FileInfo(FILEPATH); - if (file.exists()) { - file.delete(); - } - } } module.exports = Device; \ No newline at end of file diff --git a/server/devices.js b/server/devices.js new file mode 100644 index 00000000..1b9a2858 --- /dev/null +++ b/server/devices.js @@ -0,0 +1,87 @@ +const log = require('loglevel').getLogger('Devices'); +const Config = require('../config/config'); +const Device = require('./device'); +const FileInfo = require('./file-info'); +const Package = require('../package.json'); +const Process = require('./process'); +const Scanimage = require('./scanimage'); + +// Relative to execution path +const FILEPATH = './config/devices.json'; + +class Devices { + static _parseDevices(s) { + const deviceIds = []; + const pattern = /device `?(.*)'.*/g; + let match; + while ((match = pattern.exec(s)) !== null) { + deviceIds.push(match[1]); + } + return deviceIds; + } + + static from(o) { + if (typeof o === 'object') { + const devices = []; + if (Array.isArray(o)) { + for (let d of o) { + devices.push(Device.from(d)); + } + } + return devices; + } else { + throw new Error('Unexpected data for Devices'); + } + } + + /// Attempts to get a stored configuration of our devices and if + /// not gets it from the command line. + static async get() { + const file = new FileInfo(FILEPATH); + let devices = null; + + if (file.exists()) { + devices = Devices.from(file.toJson()); + if (devices.length > 0) { + if (devices[0].version !== Package.version) { + log.debug('devices.json version is old. Reloading'); + devices = null; + } + } else { + log.debug('devices.json contains no devices. Reloading'); + devices = null; + } + } else { + log.debug('devices.json does not exist. Reloading'); + } + + if (devices === null) { + let deviceIds = Config.devices; + if (Config.devicesFind) { + const data = await Process.execute(Scanimage.devices()); + log.debug('Device list: ', data); + const localDevices = Devices._parseDevices(data); + deviceIds = deviceIds.concat(localDevices); + } + + devices = []; + for (let deviceId of deviceIds) { + const data = await Process.execute(Scanimage.features(deviceId)); + log.debug('Device features: ', data); + devices.push(Device.from(data)); + } + file.save(JSON.stringify(devices, null, 2)); + } + + return devices; + } + + static reset() { + const file = new FileInfo(FILEPATH); + if (file.exists()) { + file.delete(); + } + } +} + +module.exports = Devices; \ No newline at end of file diff --git a/server/file-info.js b/server/file-info.js index 949e9226..29792047 100644 --- a/server/file-info.js +++ b/server/file-info.js @@ -39,9 +39,14 @@ class FileInfo { this.lastModified = stat.mtime; this.size = stat.size; this.sizeString = sizeString(this.size); + this.isDirectory = stat.isDirectory(); } } + static create(fullpath) { + return new FileInfo(fullpath); + } + delete() { try { fs.unlinkSync(this.fullname); @@ -61,6 +66,17 @@ class FileInfo { return fs.existsSync(this.fullname); } + async move(destination) { + return await new Promise((resolve, reject) => { + fs.rename(this.fullname, destination, (err) => { + if (err) { + reject(err); + } + resolve(); + }); + }); + } + save(data) { fs.writeFileSync(this.fullname, data); } @@ -81,6 +97,22 @@ class FileInfo { toJson() { return JSON.parse(this.toText()); } + + async list() { + return await new Promise((resolve, reject) => { + if (!this.isDirectory) { + reject(`${this.fullname} is not a directory`); + } + fs.readdir(this.fullname, (err, list) => { + if (err) { + reject(err); + } + + const files = list.map(f => new FileInfo(`${this.fullname}/${f}`)); + resolve(files); + }); + }); + } } module.exports = FileInfo; \ No newline at end of file diff --git a/server/process.js b/server/process.js index f439f226..26dec68c 100644 --- a/server/process.js +++ b/server/process.js @@ -2,6 +2,7 @@ const util = require('util'); const log = require('loglevel').getLogger('Process'); const exec = util.promisify(require('child_process').exec); const spawn = require('child_process').spawn; +const extend = require('./util').extend; const Process = { async execute(cmd) { @@ -9,19 +10,20 @@ const Process = { return stdout; }, - async spawn(cmd, stdin, ignoreErrors) { + async spawn(cmd, stdin, options) { const MAX_BUFFER = 16 * 1024; - ignoreErrors = ignoreErrors === undefined ? false : true; - log.debug(cmd, stdin, ignoreErrors); + options = extend({ + encoding: 'binary', + shell: true, + maxBuffer: MAX_BUFFER, + ignoreErrors: false + }, options); + + log.debug(`${cmd}, `, stdin, `, ${JSON.stringify(options)}`); return await new Promise((resolve, reject) => { let stdout = Buffer.alloc(0); let stderr = ''; - const proc = spawn(cmd, [], { - encoding: 'binary', - shell: true, - maxBuffer: MAX_BUFFER - }); - + const proc = spawn(cmd, [], options); proc.stdout.on('data', (data) => { stdout = Buffer.concat([stdout, data]); }); @@ -30,7 +32,7 @@ const Process = { stderr += data; }); - if (!ignoreErrors) { + if (!options.ignoreErrors) { proc.on('error', (exception) => { reject(new Error(`${cmd} error: ${exception.message}, stderr: ${stderr}`)); }); @@ -38,7 +40,7 @@ const Process = { proc.on('close', (code) => { log.debug(`close(${code}): ${cmd}`); - if (code !== 0 && !ignoreErrors) { + if (code !== 0 && !options.ignoreErrors) { reject(new Error(`${cmd} exited with code: ${code}, stderr: ${stderr}`)); } else { resolve(stdout); @@ -52,10 +54,10 @@ const Process = { }); }, - async chain(cmdArray, stdin, ignoreErrors) { + async chain(cmdArray, stdin, options) { let stdout = null; for (let cmd of cmdArray) { - stdout = await Process.spawn(cmd, stdin, ignoreErrors); + stdout = await Process.spawn(cmd, stdin, options); stdin = stdout; } return stdout; diff --git a/server/request.js b/server/request.js index aa67274f..08d6d5b7 100644 --- a/server/request.js +++ b/server/request.js @@ -1,4 +1,5 @@ const log = require('loglevel').getLogger('Request'); +const Constants = require('./constants'); const extend = require('./util').extend; const bound = (n, min, max, def) => { @@ -27,24 +28,26 @@ class Request { height: bound(data.params.height, features['-y'].limits[0], features['-y'].limits[1], features['-y'].limits[1]), resolution: data.params.resolution || features['--resolution'].default, mode: data.params.mode || features['--mode'].default, - format: 'tiff', - brightness: data.params.brightness || 0, - contrast: data.params.contrast || 0, - dynamicLineart: true + format: 'tiff' }, pipeline: data.pipeline || null, - batch: data.batch || false, + batch: data.batch || Constants.BATCH_NONE, page: data.page || 1 }); - if ('--brightness' in features === false) { - delete this.params.brightness; + if ('--source' in features) { + this.params.source = data.params.source || features['--source'].default; } - if ('--contrast' in features === false) { - delete this.params.contrast; + if ('--brightness' in features) { + this.params.brightness = data.params.brightness || 0; } - if ('--disable-dynamic-lineart' in features === false) { - delete this.params.dynamicLineart; + if ('--contrast' in features) { + this.params.contrast = data.params.contrast || 0; + } + if ('--disable-dynamic-lineart' in features) { + this.params.dynamicLineart = data.params.dynamicLineart !== undefined + ? data.params.dynamicLineart + : true; } log.debug(JSON.stringify(this)); diff --git a/server/scanimage.js b/server/scanimage.js index ff5f832a..18448ae4 100644 --- a/server/scanimage.js +++ b/server/scanimage.js @@ -2,10 +2,18 @@ const log = require('loglevel').getLogger('Scanimage'); const CmdBuilder = require('./command-builder'); const Config = require('../config/config'); +const Constants = require('./constants'); class Scanimage { - static all() { + static devices() { return new CmdBuilder(Config.scanimage) + .arg('-L') + .build(); + } + + static features(deviceId) { + return new CmdBuilder(Config.scanimage) + .arg('-d', deviceId) .arg('-A') .build(); } @@ -23,6 +31,9 @@ class Scanimage { .arg('-y', params.height) .arg('--format', params.format); + if ('source' in params) { + cmdBuilder.arg('--source', params.source); + } if ('depth' in params) { cmdBuilder.arg('--depth', params.depth); } @@ -35,7 +46,12 @@ class Scanimage { if (params.mode === 'Lineart' && params.dynamicLineart === false) { cmdBuilder.arg('--disable-dynamic-lineart=yes'); } - + if (request.batch === Constants.BATCH_AUTO) { + cmdBuilder.arg(`--batch=${Config.tempDirectory}${Constants.TEMP_FILESTEM}%04d.tif`); + } else { + const number = `000${request.page}`.slice(-4); + cmdBuilder.arg(`> ${Config.tempDirectory}${Constants.TEMP_FILESTEM}${number}.tif`); + } return cmdBuilder.build(); } } diff --git a/server/util.js b/server/util.js index 55b53a8c..eae30c84 100644 --- a/server/util.js +++ b/server/util.js @@ -1,3 +1,5 @@ +const AdmZip = require('adm-zip'); + const Util = { extend() { const t = arguments[0]; @@ -8,6 +10,14 @@ const Util = { } } return t; + }, + + zip(filepaths, destination) { + const zip = new AdmZip(); + for (let filepath of filepaths) { + zip.addLocalFile(filepath); + } + zip.writeZip(destination); } }; diff --git a/test/device.test.js b/test/device.test.js index 2848ec46..39864674 100644 --- a/test/device.test.js +++ b/test/device.test.js @@ -11,6 +11,7 @@ describe('Device', () => { assert.strictEqual(device.id, 'plustek:libusb:001:008'); assert.deepStrictEqual(device.features['--mode'].options, ['Lineart', 'Gray', 'Color']); assert.strictEqual(device.features['--mode'].default, 'Color'); + assert.strictEqual(device.features['--source'], undefined); assert.deepStrictEqual(device.features['--resolution'].options, [50, 75, 150, 300, 600, 1200]); assert.strictEqual(device.features['--resolution'].default, 50); assert.strictEqual(device.features['-l'].limits[0], 0); @@ -42,6 +43,7 @@ describe('Device', () => { assert.strictEqual(device.id, 'epson2:libusb:001:029'); assert.deepStrictEqual(device.features['--mode'].options, ['Lineart', 'Gray', 'Color']); assert.strictEqual(device.features['--mode'].default, 'Lineart'); + assert.strictEqual(device.features['--source'], undefined); assert.deepStrictEqual(device.features['--resolution'].options, [75, 300, 600, 1200]); assert.strictEqual(device.features['--resolution'].default, 75); assert.strictEqual(device.features['-l'].limits[0], 0); @@ -67,6 +69,7 @@ describe('Device', () => { assert.strictEqual(device.id, 'magic'); assert.deepStrictEqual(device.features['--mode'].options, ['Lineart', 'Gray', '24bitColor']); assert.strictEqual(device.features['--mode'].default, '24bitColor'); + assert.strictEqual(device.features['--source'], undefined); assert.deepStrictEqual(device.features['--resolution'].options, [75, 300, 600, 1200]); assert.strictEqual(device.features['--resolution'].default, 75); assert.strictEqual(device.features['-l'].limits[0], 0); @@ -94,6 +97,8 @@ describe('Device', () => { assert.strictEqual(device.id, 'net:192.168.1.4:xerox_mfp:libusb:001:003'); assert.deepStrictEqual(device.features['--mode'].options, ['Lineart', 'Halftone', 'Gray', 'Color']); assert.strictEqual(device.features['--mode'].default, 'Color'); + assert.deepStrictEqual(device.features['--source'].options, ['Flatbed', 'ADF', 'Auto']); + assert.strictEqual(device.features['--source'].default, 'Flatbed'); assert.deepStrictEqual(device.features['--resolution'].options, [75, 100, 150, 200, 300, 600, 1200]); assert.strictEqual(device.features['--resolution'].default, 150); assert.strictEqual(device.features['-l'].limits[0], 0); diff --git a/test/devices.test.js b/test/devices.test.js new file mode 100644 index 00000000..f9e2f6e4 --- /dev/null +++ b/test/devices.test.js @@ -0,0 +1,21 @@ +/* eslint-env mocha */ +const assert = require('assert'); +const Devices = require('../server/devices'); +const FileInfo = require('../server/file-info'); + +describe('Devices', () => { + it('scanimage-l1.txt', () => { + const file = new FileInfo('test/resource/scanimage-l1.txt'); + const deviceIds = Devices._parseDevices(file.toText()); + assert.deepStrictEqual(deviceIds, ['plustek:libusb:001:003']); + }); + + it('scanimage-l2.txt', () => { + const file = new FileInfo('test/resource/scanimage-l2.txt'); + const deviceIds = Devices._parseDevices(file.toText()); + assert.deepStrictEqual(deviceIds, [ + 'plustek:libusb:001:003', + 'airscan:w1:CANON INC. TR8500 series', + 'airscan:e0:Canon TR8500 series']); + }); +}); \ No newline at end of file diff --git a/test/fileinfo.test.js b/test/fileinfo.test.js index cff93b86..e6476472 100644 --- a/test/fileinfo.test.js +++ b/test/fileinfo.test.js @@ -24,4 +24,18 @@ describe('FileInfo', () => { const file2 = new FileInfo('test/resource/logo.png'); assert.strictEqual(file1.equals(file2), true); }); + + it('List file', () => { + assert.rejects(async () => new FileInfo('../package.json').list(), Error, 'Not a directory'); + }); + + it('List directory', async () => { + const files = await new FileInfo('./test/resource').list(); + assert.strictEqual(files[0].name, 'logo.png'); + assert.strictEqual(files.length > 1, true); + const stem = 'logo'; + const png = files.filter(f => new RegExp(`${stem}.png`).test(f.name)); + assert.strictEqual(png.length, 1); + }); + }); \ No newline at end of file diff --git a/test/process.test.js b/test/process.test.js index f0d64bb5..e3306c19 100644 --- a/test/process.test.js +++ b/test/process.test.js @@ -27,7 +27,23 @@ describe('Process', () => { const ls = await Process.spawn(cmd); const result = await Process.spawn('wc -l', ls); assert.strictEqual(result.toString(), '3\n'); - console.log(result); + }); + + it('error', async () => { + assert.rejects(async () => { + await Process.spawn('hello'); + }, Error, '/bin/sh: 1: hello'); + }); + + it('ignore error', async () => { + await Process.spawn('hello', null, { ignoreErrors: true }); + }); + + it('cwd', async () => { + const cmd = new CmdBuilder('echo').arg('"1\n2\n3"').build(); + assert.strictEqual(cmd, 'echo "1\n2\n3"'); + const ls = await Process.spawn('ls -al', null, { cwd: './test/resource' }); + assert.strictEqual(ls.indexOf('logo.png') > -1, true); }); it('cat ./test/resource/logo.png', async () => { @@ -35,14 +51,16 @@ describe('Process', () => { assert.strictEqual(png.length, 3451); }); - it('spawn: cat ./test/resource/logo.png | convert - -quality 50 jpg:-', async () => { + it('spawn: cat ./test/resource/logo.png | convert - -quality 50 jpg:-', async function () { + this.timeout(5000); const png = await Process.spawn('cat ./test/resource/logo.png'); const jpg = await Process.spawn('convert - -quality 50 jpg:-', png); // It should be about 4179 but different convert implementations may vary assert.strictEqual(jpg.length > 4000, true); }); - it('chain: cat ./test/resource/logo.png | convert - -quality 50 jpg:-', async () => { + it('chain: cat ./test/resource/logo.png | convert - -quality 50 jpg:-', async function () { + this.timeout(5000); const cmds = [ 'cat ./test/resource/logo.png', 'convert - -quality 50 jpg:-' @@ -52,7 +70,8 @@ describe('Process', () => { assert.strictEqual(jpg.length > 4000, true); }); - it('chain: cat ./test/resource/logo.png | convert - pdf:-', async () => { + it('chain: cat ./test/resource/logo.png | convert - pdf:-', async function () { + this.timeout(5000); const cmds = [ 'cat ./test/resource/logo.png', 'convert - pdf:-' diff --git a/test/resource/scanimage-l1.txt b/test/resource/scanimage-l1.txt new file mode 100644 index 00000000..42b6ba5b --- /dev/null +++ b/test/resource/scanimage-l1.txt @@ -0,0 +1 @@ +device `plustek:libusb:001:003' is a Canon CanoScan N670U/N676U/LiDE20 flatbed scanner \ No newline at end of file diff --git a/test/resource/scanimage-l2.txt b/test/resource/scanimage-l2.txt new file mode 100644 index 00000000..337367a8 --- /dev/null +++ b/test/resource/scanimage-l2.txt @@ -0,0 +1,3 @@ +device `plustek:libusb:001:003' is a Canon CanoScan N670U/N676U/LiDE20 flatbed scanner +device `airscan:w1:CANON INC. TR8500 series' is a WSD CANON INC. TR8500 series WSD network scanner +device `airscan:e0:Canon TR8500 series' is a eSCL Canon TR8500 series eSCL network scanner \ No newline at end of file