diff --git a/.github/workflows/publishBranchesPRs.yml b/.github/workflows/publishBranchesPRs.yml index 09148d8..31d41e7 100644 --- a/.github/workflows/publishBranchesPRs.yml +++ b/.github/workflows/publishBranchesPRs.yml @@ -5,7 +5,7 @@ name: PR - Create and publish a Docker image on: pull_request: #push: - # branches: + # branches: # - '*' # - '!master' @@ -22,10 +22,10 @@ jobs: permissions: contents: read packages: write - # + # steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 @@ -36,7 +36,7 @@ jobs: # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. - name: Extract branch name shell: bash - run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_OUTPUT + run: echo "branch=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT" id: extract_branch - name: Extract metadata (tags, labels) for Docker id: meta diff --git a/.github/workflows/publishMaster.yml b/.github/workflows/publishMaster.yml index 8abddac..1a0b6c5 100644 --- a/.github/workflows/publishMaster.yml +++ b/.github/workflows/publishMaster.yml @@ -4,7 +4,7 @@ name: Master - Create and publish a Docker image # Configures this workflow to run every time a change is pushed to the branch called `master`. on: push: - branches: ['master'] + branches: ["master"] # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: @@ -19,10 +19,10 @@ jobs: permissions: contents: read packages: write - # + # steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 diff --git a/.github/workflows/publishTags.yml b/.github/workflows/publishTags.yml index 4dd9245..903c491 100644 --- a/.github/workflows/publishTags.yml +++ b/.github/workflows/publishTags.yml @@ -4,8 +4,8 @@ name: Tags - Create and publish a Docker image # Build when a tag is created on: push: - tags: - - '**' + tags: + - "**" # Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. env: @@ -20,10 +20,10 @@ jobs: permissions: contents: read packages: write - # + # steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. - name: Log in to the Container registry uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 diff --git a/.github/workflows/superLinter.yml b/.github/workflows/superLinter.yml new file mode 100644 index 0000000..24a4e56 --- /dev/null +++ b/.github/workflows/superLinter.yml @@ -0,0 +1,47 @@ +--- +# This workflow executes several linters on changed files based on languages used in your code base whenever +# you push a code or open a pull request. +# +# You can adjust the behavior by modifying this file. +# For more information, see: +# https://github.com/github/super-linter +name: Lint Code Base + +on: # yamllint disable-line rule:truthy + push: + branches: ["master"] + pull_request: + branches: ["master"] + +permissions: + contents: read + +jobs: + run-lint: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Full git history is needed to get a proper list of changed files within `super-linter` + fetch-depth: 0 + + - name: Lint Code Base + uses: github/super-linter/slim@v7 + env: + DISABLE_ERRORS: true + VALIDATE_ALL_CODEBASE: false + VALIDATE_CHECKOV: false + VALIDATE_EDITORCONFIG: false + VALIDATE_JSCPD: false + VALIDATE_MARKDOWN: false + VALIDATE_PYTHON: true + VALIDATE_PYTHON_PYLINT: false + VALIDATE_YAML: false + VALIDATE_YAML_PRETTIER: false + DEFAULT_BRANCH: master + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + LINTER_RULES_PATH: / + DOCKERFILE_HADOLINT_FILE_NAME: .hadolint.yml + MARKDOWN_CONFIG_FILE: .markdown-lint.yml + YAML_CONFIG_FILE: .yamllint.yml diff --git a/.hadolint.yml b/.hadolint.yml new file mode 100644 index 0000000..c390dfb --- /dev/null +++ b/.hadolint.yml @@ -0,0 +1,3 @@ +ignored: + - DL3008 # Pin versions in apt get install. Instead of `apt-get install ` use `apt-get install =` + - DL3018 # Pin versions in apk add. Instead of `apk add ` use `apk add =` diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000..fb06ff6 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,11 @@ +extends: default + +rules: + document-start: disable + line-length: + max: 180 + level: warning + comments: + # Changed this to stop a mess between linters from Prettier (vscode) to yamllint + # - https://github.com/prettier/prettier/pull/10926 + min-spaces-from-content: 1 diff --git a/Dockerfile b/Dockerfile index 84d95a2..e0f4a7c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,12 @@ -FROM python:slim-bullseye +FROM python:slim-bookworm RUN <> /etc/sudoers EOF + ENV NAME="Scanner" ENV MODEL="MFC-L2700DW" ENV IPADDRESS="192.168.1.123" @@ -69,13 +71,12 @@ ENV TELEGRAM_CHATID="" # Make sure this ends in a slash. ENV FTP_PATH="/scans/" -#ADD files/gui/index.php /var/www/html -#ADD files/gui/main.css /var/www/html -#ADD files/api/scan.php /var/www/html -#ADD files/api/active.php /var/www/html -#ADD files/api/list.php /var/www/html -#ADD files/api/download.php /var/www/html -COPY html /var/www/html +EXPOSE 54925 +EXPOSE 54921 +EXPOSE 80 + +# Copy the web files to the web directory +COPY www /var/www RUN chown -R www-data /var/www/ #directory for scans: diff --git a/README.md b/README.md index 6f055ab..8f7360d 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ You can configure the tool via environment variables: | USE_JPEG_COMPRESSION | optional | use JPEG compression when creating PDFs | | TELEGRAM_TOKEN | optional | If TELEGRAM_TOKEN and TELEGRAM_CHATID are set, then this sends notification | | TELEGRAM_CHATID | optional | If TELEGRAM_TOKEN and TELEGRAM_CHATID are set, then this sends notification | +| ALLOW_GUI_FILEOPERATIONS | optional | true/false. Let you delete and rename files in files list | ### FTPS upload @@ -203,9 +204,8 @@ Thus, make sure to wait for your scan to complete, before pressing another butto #### API The GUI uses a minimal "API" at the backend, which you can also use from other tooling (e.g., Home Assistant or a control panel near your printer). -To scan, simply call `http://:/scan.php?target=` -Also check out the endpoints `list.php`, `download.php`, `active.php`. -Maybe one day an OpenAPI Spec will be included. +To scan, simply call `http://:/api/scanner/scanto/` +Also check out the swagger file in the doc directory to see all available endpoints. ## Full Docker Compose Example diff --git a/doc/swagger.yaml b/doc/swagger.yaml new file mode 100755 index 0000000..2a21525 --- /dev/null +++ b/doc/swagger.yaml @@ -0,0 +1,284 @@ +openapi: 3.0.0 +info: + title: BrotherScannerDocker API + description: API for managing and accessing scans and files. + version: 1.0.0 +servers: + - url: http://localhost:8080/api + description: Local development server +paths: + /scanner/status: + get: + summary: Returns the current status of the scanner + responses: + '200': + description: Scanner status + content: + application/json: + schema: + type: object + properties: + scan: + type: boolean + description: Indicates if a scan is in progress + waiting: + type: boolean + description: Indicates if the scanner is waiting + ocr: + type: boolean + description: Indicates if an OCR process is running + /scanner/scanto: + post: + summary: Initiates a scan operation + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + type: object + properties: + target: + type: string + description: Target for the scan operation + enum: + - file + - email + - image + - ocr + responses: + '200': + description: Scan operation successfully initiate + /scanner/scanto/{parameter}: + get: + summary: Initiates a scan operation with a specified parameter + parameters: + - name: parameter + in: path + required: true + schema: + type: string + enum: + - file + - email + - image + - ocr + responses: + '200': + description: Scan operation successfully initiated, waiting for backend response + /file-list: + get: + summary: List Scanned Files + responses: + '200': + description: Returns a list of scanned files. + content: + application/json: + schema: + type: array + items: + type: object + properties: + full_path: + type: string + file: + type: string + name: + type: string + name_clean: + type: string + dir: + type: string + date_from_name: + type: string + time_from_name: + type: string + fileCreationTime: + type: integer + fileModificationTime: + type: integer + date_from_file: + type: string + time_from_file: + type: string + extension: + type: string + mimetype: + type: string + size: + type: integer + example: + - full_path: "/scans/2024-09-21 Invoice Company A.pdf" + file: "Invoice Company A.pdf" + name: "Invoice Company A" + name_clean: "Company A Invoice" + dir: "/scans" + date_from_name: "2024-09-21" + time_from_name: "" + fileCreationTime: 1726902248 + fileModificationTime: 1726902199 + date_from_file: "2024-09-21" + time_from_file: "09-03-19" + extension: "pdf" + mimetype: "application/pdf" + size: 2970761 + - full_path: "/scans/2024-09-22 Insurance Policy Document.pdf" + file: "Insurance Policy Document.pdf" + name: "Insurance Policy Document" + name_clean: "Insurance Policy" + dir: "/scans" + date_from_name: "2024-09-22" + time_from_name: "" + fileCreationTime: 1727016766 + fileModificationTime: 1726582762 + date_from_file: "2024-09-17" + time_from_file: "16-19-22" + extension: "pdf" + mimetype: "application/pdf" + size: 2947326 + /file/{file}/info: + get: + summary: Provides extended information about a file + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: Extended file information + content: + application/json: + schema: + type: object + properties: + full_path: + type: string + description: The full path to the file + file: + type: string + description: The file name + name: + type: string + description: The file name without extension + name_clean: + type: string + description: Cleaned version of the file name + dir: + type: string + description: Directory where the file is located + date_from_name: + type: string + format: date + description: Date extracted from the file name + time_from_name: + type: string + format: time + description: Time extracted from the file name + fileCreationTime: + type: integer + description: File creation time as a Unix timestamp + fileModificationTime: + type: integer + description: File modification time as a Unix timestamp + date_from_file: + type: string + format: date + description: Date extracted from the file metadata + time_from_file: + type: string + description: Time extracted from the file metadata + extension: + type: string + description: File extension + mimetype: + type: string + description: MIME type of the file + size: + type: integer + description: Size of the file in bytes + example: + full_path: "/scans/2024-09-13-13-08-46 ganz anderes.pdf" + file: "2024-09-13-13-08-46 ganz anderes.pdf" + name: "2024-09-13-13-08-46 ganz anderes" + name_clean: "ganz anderes" + dir: "/scans" + date_from_name: "2024-09-13" + time_from_name: "13:08:46" + fileCreationTime: 1727176003 + fileModificationTime: 1726225726 + date_from_file: "2024-09-13" + time_from_file: "13-08-46" + extension: "pdf" + mimetype: "application/pdf" + size: 1161527 + /file/{file}/download: + get: + summary: Download a File + parameters: + - name: file + in: path + required: true + description: The name of the file to download. + schema: + type: string + responses: + '200': + description: The file is downloaded. + content: + application/pdf: + schema: + type: string + format: binary + /file/{file}/delete: + delete: + summary: Deletes the specified file + parameters: + - name: file + in: path + required: true + schema: + type: string + responses: + '200': + description: File successfully deleted + /file/{file}/rename: + put: + summary: Renames the specified file + parameters: + - name: file + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + new_filename: + type: string + new_filename_prefix: + type: string + enum: + - none + - date + - datetime + responses: + '200': + description: File successfully renamed + /dev/timezone: + get: + summary: Returns the current timezone + responses: + '200': + description: Current timezone + content: + application/json: + schema: + type: object + properties: + timezone: + type: string diff --git a/docker-compose-example.yml b/docker-compose-example.yml index 5809b88..5f69203 100755 --- a/docker-compose-example.yml +++ b/docker-compose-example.yml @@ -28,6 +28,7 @@ services: - OCR_PATH=ocr.php - TELEGRAM_TOKEN="" # note: keep the word bot in the string - TELEGRAM_CHATID=127585497 # note: target chat id. can be person or group + - ALLOW_GUI_FILEOPERATIONS=true restart: unless-stopped # optional, for OCR diff --git a/files/brscan-skey.config b/files/brscan-skey.config new file mode 100644 index 0000000..02029c1 --- /dev/null +++ b/files/brscan-skey.config @@ -0,0 +1,8 @@ +password= +IMAGE="python3 /opt/brother/scanner/brscan-skey/script/scantoimage.py" +OCR="python3 /opt/brother/scanner/brscan-skey/script/scantoocr.py" +EMAIL="python3 /opt/brother/scanner/brscan-skey/script/scantoemail.py" +FILE="python3 /opt/brother/scanner/brscan-skey/script/scantofile.py" +SEMID=b +eth= +ip_address= diff --git a/files/runScanner.sh b/files/runScanner.sh index 6ed2e03..f14d4bf 100755 --- a/files/runScanner.sh +++ b/files/runScanner.sh @@ -3,7 +3,7 @@ echo "setting up user & logfile:" if [[ $NAME == *" "* ]]; then echo "Do not use spaces in NAME!" - exit -1 + exit 1 fi if [[ -z ${UID} ]]; then @@ -16,8 +16,9 @@ groupadd --gid "$GID" NAS adduser "$NAME" --uid $UID --gid "$GID" --disabled-password --force-badname --gecos "" mkdir -p /scans chmod 777 /scans -touch /var/log/scanner.log +echo -n "" >/var/log/scanner.log chown "$NAME" /var/log/scanner.log +chmod 666 /var/log/scanner.log env >/opt/brother/scanner/env.txt chmod -R 777 /opt/brother echo "-----" @@ -85,18 +86,39 @@ if [ "$WEBSERVER" == "true" ]; then fi if [[ -n "$DISABLE_GUI_SCANTOOCR" ]]; then echo "\$DISABLE_GUI_SCANTOOCR=$DISABLE_GUI_SCANTOOCR;" + fi + if [[ -n "$ALLOW_GUI_FILEOPERATIONS" ]]; then + echo "\$ALLOW_GUI_FILEOPERATIONS=$ALLOW_GUI_FILEOPERATIONS;" fi echo "?>" } >/var/www/html/config.php + + + if ! grep url.rewrite-if-not-file < /etc/lighttpd/lighttpd.conf >/dev/null; then + # Add rewrite rules to the Lighttpd configuration + cat <> /etc/lighttpd/lighttpd.conf + +server.modules += ( "mod_rewrite" ) + +url.rewrite-if-not-file = ( + "^/(.*)$" => "/index.php" +) + +EOL + fi + chown www-data /var/www/html/config.php if [[ -z ${PORT} ]]; then PORT=80 fi + echo "running on port $PORT" sed -i "s/server.port\W*= 80/server.port = $PORT/" /etc/lighttpd/lighttpd.conf /usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf echo "webserver started" + + else echo "webserver not configured" fi diff --git a/html/active.php b/html/active.php deleted file mode 100644 index 2b3e4d5..0000000 --- a/html/active.php +++ /dev/null @@ -1,30 +0,0 @@ - isProcessRunning('scanimage'), - 'waiting' => isProcessRunning('sleep'), - 'ocr' => isProcessRunning('curl') -); - - -// Output the result as JSON -header('Content-Type: application/json; charset=utf-8'); -echo json_encode($result); - -?> \ No newline at end of file diff --git a/html/download.php b/html/download.php deleted file mode 100644 index 45da56a..0000000 --- a/html/download.php +++ /dev/null @@ -1,23 +0,0 @@ - \ No newline at end of file diff --git a/html/index.php b/html/index.php deleted file mode 100644 index ee283af..0000000 --- a/html/index.php +++ /dev/null @@ -1,231 +0,0 @@ - - - - - - - Brother <?php echo($MODEL); ?> - - - - - - - - - - - - - - - -
-
-
-
- -

-

Ready to scan

- - '.$button_file.'

'); - } - if (!isset($DISABLE_GUI_SCANTOEMAIL) || $DISABLE_GUI_SCANTOEMAIL != true) { - echo('

'.$button_email.'

'); - } - if (!isset($DISABLE_GUI_SCANTOIMAGE) || $DISABLE_GUI_SCANTOIMAGE != true) { - echo('

'.$button_image.'

'); - } - if (!isset($DISABLE_GUI_SCANTOOCR) || $DISABLE_GUI_SCANTOOCR != true) { - echo('

'.$button_ocr.'

'); - } - ?> -
-
-
-
- - -
- -
-
Last scanned
- -
-
- - - - - -
-
- - - - - - - - - - - \ No newline at end of file diff --git a/html/list.php b/html/list.php deleted file mode 100644 index 9f48f23..0000000 --- a/html/list.php +++ /dev/null @@ -1,55 +0,0 @@ -
- -
- - filemtime($filePath), - 'size' => filesize($filePath), - 'permissions' => substr(sprintf('%o', fileperms($filePath)), -4), - 'owner' => posix_getpwuid(fileowner($filePath))['name'], - 'group' => posix_getgrgid(filegroup($filePath))['name'], - ); - } -} - -// Sort files by modification time (newest first) -uasort($filesWithMtime, function($a, $b) { - return $b['mtime'] <=> $a['mtime']; -}); - -// Output sorted files -foreach ($filesWithMtime as $file => $attributes) { -?> - -
- - -
-
Bytes
-
- - - - -
-
- - diff --git a/html/scan.php b/html/scan.php deleted file mode 100644 index ddfa94e..0000000 --- a/html/scan.php +++ /dev/null @@ -1,46 +0,0 @@ -)"); -} -if (in_array($target, array('file','email','image','ocr'))) { - if ($_SERVER['REQUEST_METHOD'] == 'POST') { - //return immediately - $handle = popen('sudo -b -u \#'.$UID.' /opt/brother/scanner/brscan-skey/script/scanto'.$target.'.sh', 'r'); - } else if ($_SERVER['REQUEST_METHOD'] == 'GET') { - //wait for completion - $output=shell_exec('sudo -u \#'.$UID.' /opt/brother/scanner/brscan-skey/script/scanto'.$target.'.sh'); - } -} -else -{ - header($_SERVER["SERVER_PROTOCOL"] . " 400 OK"); - die("Error: Thou shalt not inject unknown script names!"); -} - -//TODO: Fix serving of file on get -//if ($_SERVER['REQUEST_METHOD'] == 'GET') { -// $files = scandir('/scans', SCANDIR_SORT_DESCENDING); -// $newest_file = $files[0]; -// header($_SERVER["SERVER_PROTOCOL"] . " 200 OK"); -// header("Cache-Control: public"); // needed for internet explorer -// header("Content-Type: application/pdf"); -// header("Content-Transfer-Encoding: Binary"); -// header("Content-Length:".filesize($newest_file)); -// readfile($newest_file); -// die(); -//} - -?> \ No newline at end of file diff --git a/html/timezone.php b/html/timezone.php deleted file mode 100755 index 7d5a4e9..0000000 --- a/html/timezone.php +++ /dev/null @@ -1,12 +0,0 @@ -"; -echo "Time: " . date("Y-m-d H:i:s"); -?> \ No newline at end of file diff --git a/script/remove_blank.sh b/script/remove_blank.sh deleted file mode 100755 index 9a25c1b..0000000 --- a/script/remove_blank.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/bash -# remove_blank - git.waldenlabs.net/calvinrw/brother-paperless-workflow -# Heavily based on from Anthony Street's (and other contributors') -# StackExchange answer: https://superuser.com/a/1307895 - -if [ -n "$REMOVE_BLANK_THRESHOLD" ]; then - IN="$1" - FILENAME="$(basename "${IN}")" - FILENAME="${FILENAME%.*}" - SCRIPTNAME="remove_blank.sh" - PAGES="$(pdfinfo "$IN" | grep ^Pages: | tr -dc '0-9')" - echo "$SCRIPTNAME: threshold=$REMOVE_BLANK_THRESHOLD; analyzing $PAGES pages" - - cd "$(dirname "$IN")" || exit - pwd - - function non_blank() { - for i in $(seq 1 "$PAGES"); do - PERCENT=$(gs -o - -dFirstPage="${i}" -dLastPage="${i}" -sDEVICE=ink_cov "$IN" | grep CMYK | nawk 'BEGIN { sum=0; } {sum += $1 + $2 + $3 + $4;} END { printf "%.5f\n", sum } ') - if [ $(echo "$PERCENT > $REMOVE_BLANK_THRESHOLD" | bc) -eq 1 ]; then - echo "$i" - echo "Page $i: keep" 1>&2 - else - echo "Page $i: delete" 1>&2 - fi - done | tee "$FILENAME.tmp" - } - - set +x - pdftk "${IN}" cat $(non_blank) output "${FILENAME}_noblank.pdf" && - mv "${FILENAME}_noblank.pdf" "$IN" -fi diff --git a/script/scanRear.sh b/script/scanRear.sh deleted file mode 100755 index 3fa0180..0000000 --- a/script/scanRear.sh +++ /dev/null @@ -1,126 +0,0 @@ -#!/bin/bash -# $1 = scanner device -# $2 = friendly name - -#override environment, as brscan is screwing it up: -export $(grep -v '^#' /opt/brother/scanner/env.txt | xargs) - -resolution="${RESOLUTION:-300}" - -gm_opts=(-page A4+0+0) -if [ "$USE_JPEG_COMPRESSION" = "true" ]; then - gm_opts+=(-compress JPEG -quality 80) -fi - -device="$1" -script_dir="/opt/brother/scanner/brscan-skey/script" -remove_blank="${script_dir}/remove_blank.sh" - -set -e # Exit on error - -mkdir -p /tmp -cd /tmp -date=$(ls -rd */ | grep "$(date +"%Y-%m-%d")" | head -1) -date=${date%/} -tmp_dir="/tmp/${date}" -filename_base="${tmp_dir}/${date}-back-page" -tmp_output_file="${filename_base}%04d.pnm" -tmp_output_pdf_file="${tmp_dir}/${date}.pdf" -output_pdf_file="/scans/${date}.pdf" - -cd "$tmp_dir" - -kill -9 "$(cat scan_pid)" -rm scan_pid - -function scan_cmd() { - # `brother4:net1;dev0` device name gets passed to scanimage, which it refuses as an invalid device name for some reason. - # Let's use the default scanner for now - # scanimage -l 0 -t 0 -x 215 -y 297 --device-name="$1" --resolution="$2" --batch="$3" - scanimage -l 0 -t 0 -x 215 -y 297 --format=pnm --resolution="$2" --batch="$3" -} - -if [ "$(which usleep 2>/dev/null)" != '' ]; then - usleep 100000 -else - sleep 0.1 -fi -scan_cmd "$device" "$resolution" "$tmp_output_file" -if [ ! -s "${filename_base}0001.pnm" ]; then - if [ "$(which usleep 2>/dev/null)" != '' ]; then - usleep 1000000 - else - sleep 1 - fi - scan_cmd "$device" "$resolution" "$tmp_output_file" -fi - -( - - #rename pages: - numberOfPages=$(find . -maxdepth 1 -name "*front-page*" | wc -l) - echo "number of pages scanned: $numberOfPages" - - cnt=0 - for filename in *front*.pnm; do - cnt=$((cnt + 1)) - cntFormatted=$(printf "%03d" $cnt) - if [[ $filename = *"front"* ]]; then - mv "$filename" "index${cntFormatted}-1-${filename}" - fi - done - cnt=0 - for filename in *back*.pnm; do - cnt=$((cnt + 1)) - if [[ $filename = *"back"* ]]; then - rearIndex=$((numberOfPages - cnt + 1)) - rearIndexFormatted=$(printf "%03d" $rearIndex) - mv "$filename" "index${rearIndexFormatted}-2-${filename}" - fi - done - - ( - echo "converting to PDF for $date..." - gm convert ${gm_opts[@]} ./*.pnm "$tmp_output_pdf_file" - ${script_dir}/trigger_inotify.sh "${SSH_USER}" "${SSH_PASSWORD}" "${SSH_HOST}" "${SSH_PATH}" "${output_pdf_file}" - ${script_dir}/trigger_telegram.sh "${date}.pdf (rear) scanned" - ${script_dir}/sendtoftps.sh \ - "${FTP_USER}" \ - "${FTP_PASSWORD}" \ - "${FTP_HOST}" \ - "${FTP_PATH}" \ - "${output_pdf_file}" - - $remove_blank "$tmp_output_pdf_file" - mv "$tmp_output_pdf_file" "$output_pdf_file" - - $script_dir/trigger_inotify.sh "${SSH_USER}" "${SSH_PASSWORD}" "${SSH_HOST}" "${SSH_PATH}" "${output_pdf_file}" - - echo "cleaning up for $date..." - cd /scans || exit - rm -rf "$tmp_dir" - - if [ -z "${OCR_SERVER}" ] || [ -z "${OCR_PORT}" ] || [ -z "${OCR_PATH}" ]; then - echo "OCR environment variables not set, skipping OCR." - else - echo "starting OCR for $date..." - ( - curl -F "userfile=@${output_pdf_file}" -H "Expect:" -o "/scans/${date}-ocr.pdf" "${OCR_SERVER}":"${OCR_PORT}"/"${OCR_PATH}" - ${script_dir}/trigger_inotify.sh "${SSH_USER}" "${SSH_PASSWORD}" "${SSH_HOST}" "${SSH_PATH}" "${date}-ocr.pdf" - ${script_dir}/trigger_telegram.sh "${date}-ocr.pdf (rear) OCR finished" - ${script_dir}/sendtoftps.sh \ - "${FTP_USER}" \ - "${FTP_PASSWORD}" \ - "${FTP_HOST}" \ - "${FTP_PATH}" \ - "/scans/${date}-ocr.pdf" - - if [ "${REMOVE_ORIGINAL_AFTER_OCR}" == "true" ]; then - if [ -f "/scans/${date}-ocr.pdf" ]; then - rm ${output_pdf_file} - fi - fi - ) & - fi - ) & -) & diff --git a/script/scanner.py b/script/scanner.py new file mode 100755 index 0000000..1b2fc0a --- /dev/null +++ b/script/scanner.py @@ -0,0 +1,552 @@ +#!/usr/bin/env python3 +# $1 = scanner device +# $2 = friendly name + +import glob +import os +import re +import shutil +import signal +import subprocess +import sys +import tempfile +import time +from datetime import datetime +from typing import List, Optional, TextIO + +from sendtoftps import sendtoftps +from trigger_inotify import trigger_inotify +from trigger_telegram import trigger_telegram + +ENVIRONMENT_FILE_PATH = "/opt/brother/scanner/env.txt" +SCRIPT_DIR = "/opt/brother/scanner/brscan-skey/script" +SCAN_DIR = "/scans" +GM_COMPRESSED_JPEG_SETTINGS = ["-compress", "JPEG", "-quality", "80"] + + +# +# Utility methods +# + +def set_status(status: str) -> None: + """ + Sets the status by creating a temporary file. + + Parameters: + status (str): The status to be set. + """ + status_file = os.path.join(tempfile.gettempdir(), f"STATUS_{status.upper()}") + with open(status_file, 'w') as temp_file: + temp_file.write(status) + + +def read_status(status: str) -> bool: + """ + Checks if the status file exists. + + Parameters: + status (str): The status to be checked. + + Returns: + bool: True if the status file exists, False otherwise. + """ + status_file = os.path.join(tempfile.gettempdir(), f"STATUS_{status.upper()}") + return os.path.exists(status_file) + + +def clear_status(status: str) -> None: + """ + Clears the status by deleting the temporary file. + + Parameters: + status (str): The status to be cleared. + """ + status_file = os.path.join(tempfile.gettempdir(), f"STATUS_{status.upper()}") + if os.path.exists(status_file): + os.remove(status_file) + + +def read_environment() -> None: + # Read the environment variables from the file, + # as brscan is screwing it up + with open(ENVIRONMENT_FILE_PATH, "r") as f: + for line in f: + if not line.startswith("#"): # Skip comments + key, value = line.strip().split("=") + os.environ[key] = value + + +def execute_command(log: TextIO, command: List[str], **kwargs) -> None: + log.flush() + print(f" DEBUG: Executing command: {command}, kwargs={kwargs}") + log.flush() + + subprocess.run(command, text=True, stdout=log, stderr=log, **kwargs) + + +def execute_command_pid(log: TextIO, command: List[str], **kwargs) -> int: + log.flush() + print(f" DEBUG: Executing command: {command}, kwargs={kwargs}") + log.flush() + + process = subprocess.Popen( + command, start_new_session=True, text=True, stdout=log, stderr=log, **kwargs + ) + return process.pid + + +def scan_cmd( + log: TextIO, device: Optional[str], output_batch: str, scanimage_args: List[str] +) -> None: + log.flush() # Required, otherwise scanimage output will appear before the already printed output + + resolution = os.environ.get("RESOLUTION", 300) + # `brother4:net1;dev0` device name gets passed to scanimage, which it refuses as an invalid device name + # for some reason. + # Let's use the default scanner for now + # fmt: off + scan_command = [ + "scanimage", + "-l", "0", "-t", "0", "-x", "215", "-y", "297", + "--format=pnm", + *scanimage_args, + f"--resolution={resolution}", + f"--batch={output_batch}", + ] + # fmt: on + execute_command(log, scan_command, check=True) + + +def notify(log: TextIO, file_path: str, message: str) -> None: + trigger_inotify( + log, + os.getenv("SSH_USER"), + os.getenv("SSH_PASSWORD"), + os.getenv("SSH_HOST"), + os.getenv("SSH_PATH"), + file_path, + ) + trigger_telegram( + log, + f"Scanner: {message}", + os.getenv("TELEGRAM_TOKEN"), + os.getenv("TELEGRAM_CHATID"), + ) + + +def latest_batch_dir() -> Optional[str]: + prefix = datetime.today().strftime("%Y-%m-%d") + dir_entries = glob.glob(os.path.join(tempfile.gettempdir(), f"{prefix}*")) + dirs = filter(os.path.isdir, dir_entries) + sorted_dirs = sorted(dirs, key=os.path.getctime) + if len(sorted_dirs) == 0: + return None + return os.path.basename(sorted_dirs[-1]) + + +def move_across_mounts(source: str, destination: str) -> None: + """Moves a file across mounts. + + Args: + source (str): The source path. + destination (str): The destination path. + """ + + try: + print(f" DEBUG: Moving {source} to {destination}") + shutil.copy2(source, destination) + os.remove(source) + except Exception as e: + print(f" ERROR: moving file - {e}") + + +def clean_job_files(log: TextIO, side: str, job_name: str): + # Cleanup temporary files + print(f" {side} side: cleaning up for {job_name}...") + shutil.rmtree(os.path.join(tempfile.gettempdir(), job_name)) + + +# +# PDF manipulation methods +# +def remove_blank_pages( + log: TextIO, input_file: str, remove_blank_threshold: float +) -> None: + """Removes blank pages from a PDF file based on a threshold. + + remove_blank - git.waldenlabs.net/calvinrw/brother-paperless-workflow + Heavily based on from Anthony Street's (and other contributors') + StackExchange answer: https://superuser.com/a/1307895 + + Args: + input_file (str): The path to the input PDF file. + remove_blank_threshold (float): The threshold for ink coverage to consider a page non-blank. + """ + + filename = os.path.splitext(os.path.basename(input_file))[0] + dirname = os.path.dirname(input_file) + + # Get the number of pages in the PDF + process = subprocess.Popen( + ["pdfinfo", input_file], stdout=subprocess.PIPE, stderr=log + ) + output, _ = process.communicate() + if process.returncode != 0: + print(f" ERROR: getting number of pages from {input_file}") + return + info = output.decode() + pages_line = re.search(r"^Pages:\s*(\d+)", info, re.MULTILINE) + if pages_line is None: + print(f" ERROR: finding number of pages in {info}") + return + page_count = int(pages_line.group(1)) + + print( + f" Analyzing {page_count} pages in {input_file} with threshold {remove_blank_threshold}%" + ) + os.chdir(dirname) + + def non_blank_pages() -> List[str]: + picked_pages: List[str] = [] + for page in range(1, page_count + 1): + # Use subprocess to run gs and get ink coverage + process = subprocess.Popen( + [ + "gs", + "-o", + "-", + f"-dFirstPage={page}", + f"-dLastPage={page}", + "-sDEVICE=ink_cov", + input_file, + ], + stdout=subprocess.PIPE, + stderr=log, + ) + output, _ = process.communicate() + ink_coverage_line = re.search( + r"^\s*([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+([\d\.]+)\s+CMYK", + output.decode(), + re.MULTILINE, + ) + if ink_coverage_line is not None: + ink_coverage = sum(map(float, ink_coverage_line.groups())) + + if ink_coverage < remove_blank_threshold: + print( + f" Page {page}: delete (ink coverage: {ink_coverage:.2f}%)" + ) + else: + picked_pages += str(page) + print(f" Page {page}: keep (ink coverage: {ink_coverage:.2f}%)") + + return picked_pages + + # Use pdftk to remove pages + try: + output_file = os.path.join(dirname, f"{filename}_noblank.pdf") + selected_pages = non_blank_pages() + command = [ + "/usr/bin/pdftk", + input_file, + "cat", + *selected_pages, + "output", + output_file, + ] + execute_command(log, command, check=True) + + removed_pages = page_count - len(selected_pages) + if removed_pages == 0: + print(f" No blank pages detected in {input_file}") + else: + os.replace(output_file, input_file) + print(f" Removed {removed_pages} blank pages and saved as {input_file}") + except FileNotFoundError: + print( + f" WARNING: '{command[0]}' executable not found. Skipping PDF manipulation." + ) + except subprocess.CalledProcessError: + print(f" ERROR: manipulating {input_file}. Skipping PDF manipulation.") + + +# +# Async job methods +# +def convert_and_post_process( + job_name: str, side: str, remove_blank_threshold: Optional[float] +) -> None: + log = sys.stdout + log.flush() + + print(f" {side} side: converting to PDF for {job_name}...") + + # Find job pages, sorted in the correct order + job_dir = os.path.join(tempfile.gettempdir(), job_name) + tmp_output_pdf_file = os.path.join(job_dir, f"{job_name}.pdf") + output_pdf_file = os.path.join(SCAN_DIR, f"{job_name}.pdf") + if side == "front": + filepath_base = os.path.join(job_dir, f"{job_name}-{side}-page") + input_files = glob.glob(f"{filepath_base}*.pnm") + else: + input_files = glob.glob(os.path.join(job_dir, "*.pnm")) + input_files.sort() + + # Convert pages to single PDF with optional JPEG compression + gm_opts = [] + if os.environ.get("USE_JPEG_COMPRESSION", "false") == "true": + gm_opts += GM_COMPRESSED_JPEG_SETTINGS + execute_command( + log, ["gm", "convert", *gm_opts, *input_files, tmp_output_pdf_file], check=True + ) + + if remove_blank_threshold: + remove_blank_pages(log, tmp_output_pdf_file, remove_blank_threshold) + + move_across_mounts(tmp_output_pdf_file, output_pdf_file) + + notify(log, output_pdf_file, f"{job_name}.pdf ({side}) scanned") + + clean_job_files(log, side, job_name) + + # Check for OCR environment variables + ocr_server = os.getenv("OCR_SERVER") + ocr_port = os.getenv("OCR_PORT") + ocr_path = os.getenv("OCR_PATH") + + if not all([ocr_server, ocr_port, ocr_path]): + print(f" {side} side: OCR environment variables not set, skipping OCR.") + else: + ocr_pdf_name = f"{job_name}-ocr.pdf" + ocr_pdf_path = os.path.join(SCAN_DIR, ocr_pdf_name) + + # Perform OCR in the background + print(f" {side} side: starting OCR for {job_name}...") + execute_command( + log, + [ + "curl", + "-F", + f"userfile=@{output_pdf_file}", + "-H", + "Expect:", + "-o", + ocr_pdf_path, + f"{ocr_server}:{ocr_port}/{ocr_path}", + ], + check=True, + ) + + notify(log, ocr_pdf_name, f"{ocr_pdf_name} ({side}) OCR finished") + + ftp_user = os.getenv("FTP_USER") + ftp_password = os.getenv("FTP_PASSWORD") + ftp_host = os.getenv("FTP_HOST") + ftp_path = os.getenv("FTP_PATH") + sendtoftps(log, ftp_user, ftp_password, ftp_host, ftp_path, ocr_pdf_path) + + if os.getenv("REMOVE_ORIGINAL_AFTER_OCR") == "true" and os.path.isfile( + ocr_pdf_path + ): + os.remove(output_pdf_file) + + print(f" {side} side: Conversion and post-processing for finished.") + print("-----------------------------------") + + +def wait_for_rear_pages_or_convert(job_name: str) -> None: + # Wait for 2 minutes in case there is a rear side scan + print(f" front side: Waiting for 2 minutes before starting file conversion for {job_name}") + try: + set_status('waiting') + time.sleep(120) + finally: + clear_status('waiting') + + convert_and_post_process(job_name, "front", None) + + +# +# Reading/writing of temp state files +# + + +def scanimage_args_path(job_dir: str) -> str: + # File where the arguments to scanimage are saved across steps in the job + return os.path.join(job_dir, ".scanimage_args") + + +def save_scanimage_args(job_dir: str, scanimage_args: List[str]) -> None: + # Save scanimage_args in a file for use with future rear side scans + path = scanimage_args_path(job_dir) + with open(path, "w") as scanimage_args_file: + for arg in scanimage_args: + scanimage_args_file.write(arg + "\n") + + +def read_scanimage_args(job_dir: str) -> List[str]: + # Read scanimage_args used for front scanning + path = scanimage_args_path(job_dir) + scanimage_args = [] + try: + with open(path, "r") as scanimage_args_file: + scanimage_args = [line.rstrip() for line in scanimage_args_file] + + os.remove(path) + except FileNotFoundError: + print(f" ERROR: scanimage_args file {path} not found.") + + return scanimage_args + + +def scan_pid_path(job_dir: str) -> str: + return os.path.join(job_dir, ".scan_pid") + + +def save_front_processing_pid(job_dir: str, pid: int) -> None: + with open(scan_pid_path(job_dir), "w") as pid_file: + pid_file.write(str(pid)) + + +def kill_front_processing_from_pid(job_dir: str) -> Optional[int]: + path = scan_pid_path(job_dir) + pid = None + try: + with open(path, "r") as scan_pid_file: + pid = int(scan_pid_file.read().strip()) + print( + f" rear side: Read pid from {path}, killing front processing job {pid}" + ) + os.kill(pid, signal.SIGKILL) + except FileNotFoundError: + print(" rear side: ERROR: scan_pid file {path} not found.") + except ProcessLookupError: + print(" rear side: ERROR: process with pid {pid} not found.") + else: + os.remove(path) + return pid + + return None + + +# +# Scan entry points +# +def scan_front(log: TextIO, device: Optional[str], scanimage_args=[]) -> None: + # Clear any existing waiting status before starting a new scan + clear_status('waiting') + + # Generate unique timestamp + job_name = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + job_dir = os.path.join(tempfile.gettempdir(), job_name) + filepath_base = os.path.join(job_dir, f"{job_name}-front-page") + tmp_output_batch = f"{filepath_base}%04d.pnm" + + # Create temporary directory + os.makedirs(job_dir, exist_ok=True) + os.chdir(job_dir) + print(f"- Scanning front to batch {tmp_output_batch}") + + # Save scanimage_args in a file for use with future rear side scans + save_scanimage_args(job_dir, scanimage_args) + + # Perform scan with retry + try: + time.sleep(0.1) + scan_cmd(log, device, tmp_output_batch, scanimage_args) + if not os.path.exists(f"{filepath_base}0001.pnm"): + time.sleep(1) # Short delay before retry + scan_cmd(log, device, tmp_output_batch, scanimage_args) + except subprocess.CalledProcessError: + print(" ERROR: Cancelling scanning!") + clean_job_files(log, 'front', job_name) + return + + # Run conversion process in the background + pid = os.fork() + if pid == 0: # Child process + wait_for_rear_pages_or_convert(job_name) + os._exit(0) # Exit child process cleanly + elif pid > 0: + save_front_processing_pid(job_dir, pid) + print( + f" front side: INFO: Waiting to start conversion process for {job_name} in process with PID {pid}" + ) + else: + print(f" front side: ERROR: Fork failed ({pid}).") + + +def scan_back(log: TextIO, device: Optional[str], scanimage_args=None) -> None: + # Find latest directory in temp directory + job_name = latest_batch_dir() + print(f"- Scanning rear to latest batch {job_name}") + if job_name is None: + print(" rear side: ERROR: Could not find front scan directory") + return + + print(f" rear side: Found front-side batch: {job_name}") + job_dir = os.path.join(tempfile.gettempdir(), job_name) + filepath_base = os.path.join(job_dir, f"{job_name}-back-page") + tmp_output_batch = f"{filepath_base}%04d.pnm" + + os.chdir(job_dir) + + # Interrupt front scanning process which is waiting from a rear side scan + if kill_front_processing_from_pid(job_dir) is None: + return + + if scanimage_args is None: + # Read scanimage_args used for front scanning + scanimage_args = read_scanimage_args(job_dir) + + # Perform scan with retry + try: + time.sleep(0.1) + scan_cmd(log, device, tmp_output_batch, scanimage_args) + if not os.path.exists(f"{filepath_base}0001.pnm"): + time.sleep(1) # Short delay before retry + scan_cmd(log, device, tmp_output_batch, scanimage_args) + except subprocess.CalledProcessError: + print(" ERROR: Cancelling scanning!") + clean_job_files(log, "back", job_name) + return + + # Clear waiting status since rear pages are now scanned + clear_status('waiting') + + # Rename pages + number_of_pages = len( + [f for f in os.listdir(".") if (os.path.isfile(f) and "front-page" in f)] + ) + print(f" rear side: INFO: number of pages scanned: {number_of_pages}") + + cnt = 0 + for filename in sorted(glob.glob("*front*.pnm")): + cnt += 1 + cnt_formatted = f"{cnt:03d}" + new_filename = f"index{cnt_formatted}-1-{filename}" + os.rename(filename, new_filename) + print(f" rear side: DEBUG: renamed {filename} to {new_filename}") + + cnt = 0 + for filename in sorted(glob.glob("*back*.pnm")): + cnt += 1 + rear_index = number_of_pages - cnt + 1 + rear_index_formatted = f"{rear_index:03d}" + new_filename = f"index{rear_index_formatted}-2-{filename}" + os.rename(filename, new_filename) + print(f" rear side: DEBUG: renamed {filename} to {new_filename}") + + # Convert to PDF + remove_blank_threshold_str = os.getenv("REMOVE_BLANK_THRESHOLD") + remove_blank_threshold = None + if remove_blank_threshold_str is not None and remove_blank_threshold_str != "": + remove_blank_threshold = float(remove_blank_threshold_str) + + pid = os.fork() + if pid == 0: # Child process + convert_and_post_process(job_name, "rear", remove_blank_threshold) + os._exit(0) # Exit child process cleanly + + elif pid < 0: + print(f" rear side: ERROR: Fork failed ({pid}).") diff --git a/script/scantoemail-0.2.4-1.sh b/script/scantoemail-0.2.4-1.sh deleted file mode 100755 index 2090633..0000000 --- a/script/scantoemail-0.2.4-1.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/bash -# $1 = scanner device -# $2 = friendly name - -{ - echo "scantoemail.sh triggered" - #override environment, as brscan is screwing it up: - export $(grep -v '^#' /opt/brother/scanner/env.txt | xargs) - - SCRIPTPATH="$( - cd "$(dirname "$0")" || exit - pwd -P - )" - /bin/bash "$SCRIPTPATH"/scanRear.sh $@ - -} >>/var/log/scanner.log 2>&1 diff --git a/script/scantoemail.py b/script/scantoemail.py new file mode 100755 index 0000000..8cdc4d1 --- /dev/null +++ b/script/scantoemail.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# $1 = scanner device +# $2 = friendly name + +import sys + +from scanner import read_environment, scan_back + +if __name__ == "__main__": + # Open the log file in append mode + with open("/var/log/scanner.log", "a") as log: + # Redirect stdout to the log file + sys.stdout = log + sys.stderr = log + + read_environment() + + device = None + if len(sys.argv) > 1: + device = sys.argv[1] + scan_back(log, device) diff --git a/script/scantoemail.sh b/script/scantoemail.sh deleted file mode 120000 index 36826b0..0000000 --- a/script/scantoemail.sh +++ /dev/null @@ -1 +0,0 @@ -scantoemail-0.2.4-1.sh \ No newline at end of file diff --git a/script/scantofile-0.2.4-1.sh b/script/scantofile-0.2.4-1.sh deleted file mode 100755 index 4e8c770..0000000 --- a/script/scantofile-0.2.4-1.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash -# $1 = scanner device -# $2 = friendly name - -{ - #override environment, as brscan is screwing it up: - export $(grep -v '^#' /opt/brother/scanner/env.txt | xargs) - - resolution="${RESOLUTION:-300}" - - gm_opts=(-page A4+0+0) - if [ "$USE_JPEG_COMPRESSION" = "true" ]; then - gm_opts+=(-compress JPEG -quality 80) - fi - - device="$1" - date=$(date +%Y-%m-%d-%H-%M-%S) - script_dir="/opt/brother/scanner/brscan-skey/script" - tmp_dir="/tmp/$date" - filename_base="${tmp_dir}/${date}-front-page" - tmp_output_file="${filename_base}%04d.pnm" - output_pdf_file="/scans/${date}.pdf" - - set -e # Exit on error - - mkdir -p "$tmp_dir" - cd "$tmp_dir" - filename_base="/tmp/${date}/${date}-front-page" - output_file="${filename_base}%04d.pnm" - echo "filename: $tmp_output_file" - - function scan_cmd() { - # `brother4:net1;dev0` device name gets passed to scanimage, which it refuses as an invalid device name for some reason. - # Let's use the default scanner for now - # scanimage -l 0 -t 0 -x 215 -y 297 --device-name="$1" --resolution="$2" --batch="$3" - scanimage -l 0 -t 0 -x 215 -y 297 --format=pnm --resolution="$2" --batch="$3" - } - - if [ "$(which usleep 2>/dev/null)" != '' ]; then - usleep 100000 - else - sleep 0.1 - fi - scan_cmd "$device" "$resolution" "$tmp_output_file" - if [ ! -s "${filename_base}0001.pnm" ]; then - if [ "$(which usleep 2>/dev/null)" != '' ]; then - usleep 1000000 - else - sleep 1 - fi - scan_cmd "$device" "$resolution" "$tmp_output_file" - fi - - #only convert when no back pages are being scanned: - ( - if [ "$(which usleep 2>/dev/null)" != '' ]; then - usleep 120000000 - else - sleep 120 - fi - - ( - echo "converting to PDF for $date..." - gm convert ${gm_opts[@]} "$filename_base"*.pnm "$output_pdf_file" - ${script_dir}/trigger_inotify.sh "${SSH_USER}" "${SSH_PASSWORD}" "${SSH_HOST}" "${SSH_PATH}" "${output_pdf_file}" - ${script_dir}/trigger_telegram.sh "${date}.pdf (front) scanned" - ${script_dir}/sendtoftps.sh \ - "${FTP_USER}" \ - "${FTP_PASSWORD}" \ - "${FTP_HOST}" \ - "${FTP_PATH}" \ - "${output_pdf_file}" - - echo "cleaning up for $date..." - cd /scans || exit - rm -rf "$tmp_dir" - - if [ -z "${OCR_SERVER}" ] || [ -z "${OCR_PORT}" ] || [ -z "${OCR_PATH}" ]; then - echo "OCR environment variables not set, skipping OCR." - else - echo "starting OCR for $date..." - ( - curl -F "userfile=@${output_pdf_file}" -H "Expect:" -o "/scans/${date}-ocr.pdf" "${OCR_SERVER}":"${OCR_PORT}"/"${OCR_PATH}" - ${script_dir}/trigger_inotify.sh "${SSH_USER}" "${SSH_PASSWORD}" "${SSH_HOST}" "${SSH_PATH}" "${date}-ocr.pdf" - ${script_dir}/trigger_telegram.sh "${date}-ocr.pdf (front) OCR finished" - ${script_dir}/sendtoftps.sh \ - "${FTP_USER}" \ - "${FTP_PASSWORD}" \ - "${FTP_HOST}" \ - "${FTP_PATH}" \ - "/scans/${date}-ocr.pdf" - - if [ "${REMOVE_ORIGINAL_AFTER_OCR}" == "true" ]; then - if [ -f "/scans/${date}-ocr.pdf" ]; then - rm ${output_pdf_file} - fi - fi - ) & - fi - ) & - ) & - echo $! >scan_pid - echo "conversion process for $date is running in PID: $(cat scan_pid)" - -} >>/var/log/scanner.log 2>&1 diff --git a/script/scantofile.py b/script/scantofile.py new file mode 100755 index 0000000..2dd1105 --- /dev/null +++ b/script/scantofile.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# $1 = scanner device +# $2 = friendly name + +import sys + +from scanner import read_environment, scan_front + +if __name__ == "__main__": + # Open the log file in append mode + with open("/var/log/scanner.log", "a") as log: + # Redirect stdout to the log file + sys.stdout = log + sys.stderr = log + + read_environment() + + device = None + if len(sys.argv) > 1: + device = sys.argv[1] + scan_front(log, device) diff --git a/script/scantofile.sh b/script/scantofile.sh deleted file mode 120000 index 33f00b8..0000000 --- a/script/scantofile.sh +++ /dev/null @@ -1 +0,0 @@ -scantofile-0.2.4-1.sh \ No newline at end of file diff --git a/script/scantoimage-0.2.4-1.sh b/script/scantoimage-0.2.4-1.sh deleted file mode 100755 index fc86ca2..0000000 --- a/script/scantoimage-0.2.4-1.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# $1 = scanner device -# $2 = friendly name - -{ - - echo "ERROR!" - echo "This function is not implemented." - echo "You may implement your own script and mount under $0." - echo "Check out scripts in same folder or https://github.com/PhilippMundhenk/BrotherScannerDocker for examples." - -} >>/var/log/scanner.log 2>&1 diff --git a/script/scantoimage.py b/script/scantoimage.py new file mode 100755 index 0000000..37649c7 --- /dev/null +++ b/script/scantoimage.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python3 +# $1 = scanner device +# $2 = friendly name + +import sys + +if __name__ == "__main__": + # Open the log file in append mode + with open("/var/log/scanner.log", "a") as log: + # Redirect stdout to the log file + sys.stdout = log + sys.stderr = log + + print("ERROR!") + print("This function is not implemented.") + print("You may implement your own script and mount under $0.") + print( + "Check out scripts in same folder or https://github.com/PhilippMundhenk/BrotherScannerDocker for examples." + ) diff --git a/script/scantoimage.sh b/script/scantoimage.sh deleted file mode 120000 index bfe2ea3..0000000 --- a/script/scantoimage.sh +++ /dev/null @@ -1 +0,0 @@ -scantoimage-0.2.4-1.sh \ No newline at end of file diff --git a/script/scantoocr-0.2.4-1.sh b/script/scantoocr-0.2.4-1.sh deleted file mode 100755 index fc86ca2..0000000 --- a/script/scantoocr-0.2.4-1.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -# $1 = scanner device -# $2 = friendly name - -{ - - echo "ERROR!" - echo "This function is not implemented." - echo "You may implement your own script and mount under $0." - echo "Check out scripts in same folder or https://github.com/PhilippMundhenk/BrotherScannerDocker for examples." - -} >>/var/log/scanner.log 2>&1 diff --git a/script/scantoocr.py b/script/scantoocr.py new file mode 100755 index 0000000..652110e --- /dev/null +++ b/script/scantoocr.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# $1 = scanner device +# $2 = friendly name + +import sys + +from scanner import read_environment, scan_front + +if __name__ == "__main__": + # Open the log file in append mode + with open("/var/log/scanner.log", "a") as log: + # Redirect stdout to the log file + sys.stdout = log + sys.stderr = log + + read_environment() + + device = None + if len(sys.argv) > 1: + device = sys.argv[1] + scan_front(log, device, ["--mode=True Gray"]) diff --git a/script/scantoocr.sh b/script/scantoocr.sh deleted file mode 120000 index 105eb99..0000000 --- a/script/scantoocr.sh +++ /dev/null @@ -1 +0,0 @@ -scantoocr-0.2.4-1.sh \ No newline at end of file diff --git a/script/sendtoftps.py b/script/sendtoftps.py new file mode 100755 index 0000000..27ccd8a --- /dev/null +++ b/script/sendtoftps.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +import subprocess +from typing import List, Optional, TextIO + + +def sendtoftps( + log: TextIO, + user: Optional[str], + password: Optional[str], + address: Optional[str], + filepath: Optional[str], + file: Optional[str], +) -> None: + """Uploads a file to an FTP server. + + Args: + user (str): The FTP username. + password (str): The FTP password. + address (str): The FTP address. + filepath (str): The file path on the FTP server. + file (str): The file to upload. + """ + + if not all([user, password, address, filepath, file]): + return + + command: List[str] = [ + "curl", + "--silent", + "--show-error", + "--ssl-reqd", + "--user", + f"{user}:{password}", + "--upload-file", + str(file), + f"ftp://{address}{filepath}", + ] + + try: + subprocess.run(command, check=True, stdout=log, stderr=log) + print(f"Uploading to FTP server {address} successful.") + except subprocess.CalledProcessError: + print("Uploading to FTP failed while using curl") + print(f"user: {user}") + print(f"address: {address}") + print(f"filepath: {filepath}") + print(f"file: {file}") + exit(1) diff --git a/script/sendtoftps.sh b/script/sendtoftps.sh deleted file mode 100755 index f2106ec..0000000 --- a/script/sendtoftps.sh +++ /dev/null @@ -1,28 +0,0 @@ -user=$1 -password=$2 -address=$3 -filepath=$4 -file=$5 - -cd /scans - -if [ -z "${user}" ] || [ -z "${password}" ] || [ -z "${address}" ] || [ -z "${filepath}" ] || [ -z "${file}" ]; then - echo "FTP environment variables not set, skipping inotify trigger." -else - if curl --silent \ - --show-error \ - --ssl-reqd \ - --user "${user}:${password}" \ - --upload-file "${file}" \ - "ftp://${address}${filepath}" ; then - echo "Uploading to ftp server ${address} successful." - else - echo "Uploading to ftp failed while using curl" - echo "user: ${user}" - echo "address: ${address}" - echo "filepath: ${filepath}" - echo "file: ${file}" - exit 1 - fi -fi - diff --git a/script/trigger_inotify.py b/script/trigger_inotify.py new file mode 100755 index 0000000..9dcfad4 --- /dev/null +++ b/script/trigger_inotify.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 + +import subprocess +from typing import Optional, TextIO + + +def trigger_inotify( + log: TextIO, + user: Optional[str], + password: Optional[str], + address: Optional[str], + filepath: Optional[str], + file: Optional[str], +) -> None: + """Triggers inotify for a file. + + Args: + user (str): The SSH username. + password (str): The SSH password. + address (str): The SSH address. + filepath (str): The file path. + file (str): The file name. + """ + + if not all([user, password, address, filepath]): + print(" INFO: SSH environment variables not set, skipping inotify trigger.") + return + + command = [ + "sshpass", + "-p", + password, + "ssh", + "-o", + "StrictHostKeyChecking=no", + f"{user}@{address}", + f'sed "" -i {filepath}/{file}', + ] + + try: + subprocess.run(command, check=True, stdout=log, stderr=log) + print("Trigger inotify successful") + except subprocess.CalledProcessError: + print("Trigger inotify failed") + exit(1) diff --git a/script/trigger_inotify.sh b/script/trigger_inotify.sh deleted file mode 100755 index 4077b22..0000000 --- a/script/trigger_inotify.sh +++ /dev/null @@ -1,16 +0,0 @@ -user=$1 -password=$2 -address=$3 -filepath=$4 -file=$5 - -if [ -z "${user}" ] || [ -z "${password}" ] || [ -z "${address}" ] || [ -z "${filepath}" ]; then - echo "SSH environment variables not set, skipping inotify trigger." -else - if sshpass -p "$password" ssh -o StrictHostKeyChecking=no "$user"@"$address" "sed \"\" -i $filepath/$file"; then - echo "trigger inotify successful" - else - echo "trigger inotify failed" - exit 1 - fi -fi diff --git a/script/trigger_telegram.py b/script/trigger_telegram.py new file mode 100755 index 0000000..839089c --- /dev/null +++ b/script/trigger_telegram.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 + +import urllib.parse +from typing import Optional, TextIO + +import requests + + +def trigger_telegram( + log: TextIO, message: str, token: Optional[str], chat_id: Optional[str] +) -> None: + """Sends a Telegram message using the provided token and chat ID.""" + + if not token or not chat_id: + print( + " INFO: TELEGRAM_TOKEN or TELEGRAM_CHATID environment variables not set, skipping Telegram trigger." + ) + return + + # URL encode the message + encoded_message = urllib.parse.quote(message, safe="") + + # Build the URL + url = f"https://api.telegram.org/{token}/sendMessage" + + # Prepare data payload + payload = {"chat_id": chat_id, "text": encoded_message} + + try: + response = requests.post(url, json=payload) + response.raise_for_status() # Raise an exception for non-200 response + print(" Telegram message sent successfully.") + except requests.exceptions.RequestException as e: + print(f" ERROR: sending Telegram message: {e}") diff --git a/script/trigger_telegram.sh b/script/trigger_telegram.sh deleted file mode 100755 index ea4e3c2..0000000 --- a/script/trigger_telegram.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash - -# Check if TELEGRAM_TOKEN and TELEGRAM_CHATID are both set -if [ -z "${TELEGRAM_TOKEN}" ] || [ -z "${TELEGRAM_CHATID}" ]; then - echo "TELEGRAM_TOKEN or TELEGRAM_CHATID environment variables not set, skipping Telegram trigger." -else - # Use the environment variables TELEGRAM_TOKEN and TELEGRAM_CHATID - TOKEN="$TELEGRAM_TOKEN" - CHAT_ID="$TELEGRAM_CHATID" - - # The message is passed as a parameter - MESSAGE="Scanner: $1" - - # URL encode the message to handle spaces and special characters - ENCODED_MESSAGE=$(echo "$MESSAGE" | jq -sRr @uri) - - # Send the message using wget - wget -qO- --post-data="chat_id=$CHAT_ID&text=$ENCODED_MESSAGE" "https://api.telegram.org/$TOKEN/sendMessage" >/dev/null -fi diff --git a/update-container.sh b/update-container.sh index 11eece0..901f0e2 100755 --- a/update-container.sh +++ b/update-container.sh @@ -1,2 +1,4 @@ -docker cp ./html brotherscannerdocker-brother-scanner-1:/var/www/ -docker cp ./script brotherscannerdocker-brother-scanner-1:/opt/brother/scanner/brscan-skey/ \ No newline at end of file +docker cp ./www brotherscannerdocker-brother-scanner-1:/var/ +docker exec brotherscannerdocker-brother-scanner-1 chown -R www-data:root /var/www/ +docker cp ./script brotherscannerdocker-brother-scanner-1:/opt/brother/scanner/brscan-skey/ +docker exec brotherscannerdocker-brother-scanner-1 chown -R root:root /opt/brother/scanner/brscan-skey/ \ No newline at end of file diff --git a/html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js b/www/html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js similarity index 100% rename from html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js rename to www/html/assets/bootstrap.5.1.3/bootstrap.bundle.min.js diff --git a/html/assets/bootstrap.5.1.3/bootstrap.min.css b/www/html/assets/bootstrap.5.1.3/bootstrap.min.css similarity index 100% rename from html/assets/bootstrap.5.1.3/bootstrap.min.css rename to www/html/assets/bootstrap.5.1.3/bootstrap.min.css diff --git a/html/assets/fontawesome.5.15.4/LICENSE.txt b/www/html/assets/fontawesome.5.15.4/LICENSE.txt similarity index 100% rename from html/assets/fontawesome.5.15.4/LICENSE.txt rename to www/html/assets/fontawesome.5.15.4/LICENSE.txt diff --git a/html/assets/fontawesome.5.15.4/css/all.min.css b/www/html/assets/fontawesome.5.15.4/css/all.min.css similarity index 100% rename from html/assets/fontawesome.5.15.4/css/all.min.css rename to www/html/assets/fontawesome.5.15.4/css/all.min.css diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.eot diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.svg diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.ttf diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 b/www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-brands-400.woff2 diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.eot diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.svg diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.ttf diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 b/www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-regular-400.woff2 diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.eot diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.svg diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.ttf diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff diff --git a/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 b/www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 similarity index 100% rename from html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 rename to www/html/assets/fontawesome.5.15.4/webfonts/fa-solid-900.woff2 diff --git a/html/assets/jquery.3.7.1/jquery.min.js b/www/html/assets/jquery.3.7.1/jquery.min.js similarity index 100% rename from html/assets/jquery.3.7.1/jquery.min.js rename to www/html/assets/jquery.3.7.1/jquery.min.js diff --git a/www/html/assets/scripts.js b/www/html/assets/scripts.js new file mode 100755 index 0000000..db1a36a --- /dev/null +++ b/www/html/assets/scripts.js @@ -0,0 +1,302 @@ +function set_state_idle() { + $('#status-image').html(''); + $('#status-text').text('Ready to scan'); + $('.trigger-scan').removeClass('disabled'); +} + +function set_state_waiting() { + $('#status-image').html(''); + $('#status-text').text('Waiting for rear pages'); + $('.trigger-scan').removeClass('disabled'); +} + +function set_state_scan() { + let spinnerimage = ''; + if (spinnerimage != $('#status-image').html()) { + $('#status-image').html(spinnerimage); + } + $('#status-text').text('Scan in progress'); + $('.trigger-scan').addClass('disabled'); +} + +function set_state_ocr() { + $('#status-image').html(''); + $('#status-text').text('OCR in progress'); + $('.trigger-scan').removeClass('disabled'); +} + +function set_state(state) { + switch (state) { + case 'idle': + set_state_idle(); + break; + case 'waiting': + set_state_waiting(); + break; + case 'scan': + set_state_scan(); + break; + case 'ocr': + set_state_ocr(); + break; + default: + set_state_idle(); + } +} + +function load_files_offcanvas(){ + $.ajax({ + url: '/list-files', + method: 'GET', + success: function(response) { + // Populate the Offcanvas with the response content + $('#offcanvasContent').html(response); + + }, + error: function(xhr, status, error) { + console.error('Failed to load content:', error); + } + }); +} + +$(document).ready(function() { + + + $('.trigger-scan').click(function() { + var target = $(this).data('trigger'); + console.log('Triggered click event on element with class "trigger-scan" and data-trigger "' + target + '"'); + $.post('/api/scanner/scanto', { + target: target + }, function(data) { + console.log(data); + $(this).blur(); + + }); + }); + + + setInterval(function() { + $.get('/api/scanner/status', function(data) { + + + let state = 'idle'; + + + if (data.ocr && data.waiting && !data.scan) { + state = 'ocr'; + } else if (data.scan && data.waiting) { + state = 'scan'; + } else if (data.scan) { + state = 'scan'; + } else if (data.ocr && !data.scan) { + state = 'ocr'; + } else if (!data.ocr && !data.scan && data.waiting) { + state = 'waiting'; + } else if (!data.ocr && !data.scan && !data.waiting) { + state = 'idle'; + } + set_state(state); + }); + }, 1000); + + + + $('#triggerFiles').on('click', function(e) { + e.preventDefault(); + load_files_offcanvas(); + var offcanvas = new bootstrap.Offcanvas($('#offcanvasFiles')[0]); + offcanvas.show(); + + }); + + + + +}); + +function toggle_file_rename(source_element){ + var parentDiv = source_element.closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + + $("#"+parentId+" .file-info-label-default").toggleClass('d-none'); + $("#"+parentId+" .file-info-label-rename").toggleClass('d-none'); + + $("#"+parentId+" .file-name").toggleClass('d-none'); + $("#"+parentId+" .file-name-new").toggleClass('d-none'); + + $("#"+parentId+" .file-buttons-default").toggleClass('d-none'); + $("#"+parentId+" .file-buttons-rename").toggleClass('d-none'); + + $("#"+parentId+" .file-rename-prefix-date").checked = true; +} + +function toggle_file_delete(source_element){ + var parentDiv = source_element.closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + + $("#"+parentId+" .file-info-label-default").toggleClass('d-none'); + $("#"+parentId+" .file-info-label-delete").toggleClass('d-none'); + + $("#"+parentId+" .file-buttons-default").toggleClass('d-none'); + $("#"+parentId+" .file-buttons-delete").toggleClass('d-none'); +} + + +function toggle_file_error(parentId, message){ + + $('#'+parentId).addClass('bg-danger text-white'); + + var html =`
+ `+message+` + +
`; + $('#'+parentId).html(html); + +} + + +function toggle_file_success(parentId, message){ + + $('#'+parentId).addClass('bg-success text-white'); + + var html =`
+ `+message+` + +
`; + $('#'+parentId).html(html); + +} + + +$(document).on("click", ".refresh-files", function (e) { + e.preventDefault(); + load_files_offcanvas(); + +}); + + +$(document).on("click", ".file-rename", function (e) { + e.preventDefault(); + toggle_file_rename($(this)); + +}); + +$(document).on("click", ".file-rename-confirm", function (e) { + e.preventDefault(); + url = $(this).attr('href'); + var parentDiv = $(this).closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + var filename = $("#"+parentId+" .file-name-original").val(); + var new_filename = $("#"+parentId+" .file-name-new").val(); + + + var new_filename_prefix = 'none'; + + if($("#"+parentId+" .file-rename-prefix-none").is(':checked')) { + new_filename_prefix = 'none'; + } + if($("#"+parentId+" .file-rename-prefix-date").is(':checked')) { + new_filename_prefix = 'date'; + } + if($("#"+parentId+" .file-rename-prefix-datetime").is(':checked')) { + new_filename_prefix = 'datetime'; + } + + + console.log('url: '+url); + console.log('filename: '+filename); + console.log('new_filename: '+new_filename); + console.log('new_filename_prefix: '+new_filename_prefix); + + + if (new_filename == '' && new_filename_prefix == 'none') { + alert('Please enter a new filename or select a prefix'); + }else{ + + + payload = { + 'new_filename': new_filename, + 'new_filename_prefix': new_filename_prefix + }; + console.log(payload); + $.ajax({ + url: url, + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(payload), + success: function(response) { + + console.log(response); + console.log('File renamed'); + toggle_file_rename($(this)); + load_files_offcanvas(); + }, + error: function(xhr, status, error) { + + console.log('File NOT renamed'); + toggle_file_error(parentId, 'Rename failed'); + } + }); + + } + + + + + +}); + +$(document).on("click", ".file-rename-cancel", function (e) { + e.preventDefault(); + toggle_file_rename($(this)); +}); + + + +$(document).on("click", ".file-delete", function (e) { + e.preventDefault(); + toggle_file_delete($(this)); +}); + +$(document).on("click", ".file-delete-confirm", function (e) { + e.preventDefault(); + var parentDiv = $(this).closest('.list-group-item'); + var parentId = parentDiv.attr('id'); + + url = $(this).attr('href'); + $.ajax({ + url: url, + type: 'DELETE', + success: function(response) { + toggle_file_success(parentId, 'File deleted'); + setTimeout(() => { + load_files_offcanvas(); + }, 500); + + + }, + error: function(response, xhr, status, error) { + toggle_file_error(parentId, 'Delete failed'); + } + }); + +}); + +$(document).on("click", ".file-delete-cancel", function (e) { + e.preventDefault(); + toggle_file_delete($(this)); +}); + + + +//document.addEventListener('DOMContentLoaded', function () { +// var offcanvasElement = document.getElementById('offcanvasExample'); +// offcanvasElement.addEventListener('shown.bs.offcanvas', function () { +// // Trigger any necessary initialization here +// // For example, you can reinitialize the radio buttons or any other components +// console.log('Offcanvas is shown'); +// }); +//}); diff --git a/www/html/assets/scripts.min.js b/www/html/assets/scripts.min.js new file mode 100755 index 0000000..08ab578 --- /dev/null +++ b/www/html/assets/scripts.min.js @@ -0,0 +1 @@ +function set_state_idle(){$("#status-image").html(''),$("#status-text").text("Ready to scan"),$(".trigger-scan").removeClass("disabled")}function set_state_waiting(){$("#status-image").html(''),$("#status-text").text("Waiting for rear pages"),$(".trigger-scan").removeClass("disabled")}function set_state_scan(){let spinnerimage='';spinnerimage!=$("#status-image").html()&&$("#status-image").html(spinnerimage),$("#status-text").text("Scan in progress"),$(".trigger-scan").addClass("disabled")}function set_state_ocr(){$("#status-image").html(''),$("#status-text").text("OCR in progress"),$(".trigger-scan").removeClass("disabled")}function set_state(state){switch(state){case"idle":set_state_idle();break;case"waiting":set_state_waiting();break;case"scan":set_state_scan();break;case"ocr":set_state_ocr();break;default:set_state_idle()}}function load_files_offcanvas(){$.ajax({url:"/list-files",method:"GET",success:function(response){$("#offcanvasContent").html(response)},error:function(xhr,status,error){console.error("Failed to load content:",error)}})}function toggle_file_rename(source_element){var parentDiv,parentId=source_element.closest(".list-group-item").attr("id");$("#"+parentId+" .file-info-label-default").toggleClass("d-none"),$("#"+parentId+" .file-info-label-rename").toggleClass("d-none"),$("#"+parentId+" .file-name").toggleClass("d-none"),$("#"+parentId+" .file-name-new").toggleClass("d-none"),$("#"+parentId+" .file-buttons-default").toggleClass("d-none"),$("#"+parentId+" .file-buttons-rename").toggleClass("d-none"),$("#"+parentId+" .file-rename-prefix-date").checked=!0}function toggle_file_delete(source_element){var parentDiv,parentId=source_element.closest(".list-group-item").attr("id");$("#"+parentId+" .file-info-label-default").toggleClass("d-none"),$("#"+parentId+" .file-info-label-delete").toggleClass("d-none"),$("#"+parentId+" .file-buttons-default").toggleClass("d-none"),$("#"+parentId+" .file-buttons-delete").toggleClass("d-none")}function toggle_file_error(parentId,message){$("#"+parentId).addClass("bg-danger text-white");var html='
\n '+message+'\n \n
';$("#"+parentId).html(html)}function toggle_file_success(parentId,message){$("#"+parentId).addClass("bg-success text-white");var html='
\n '+message+'\n \n
';$("#"+parentId).html(html)}$(document).ready((function(){$(".trigger-scan").click((function(){var target=$(this).data("trigger");console.log('Triggered click event on element with class "trigger-scan" and data-trigger "'+target+'"'),$.post("/api/scanner/scanto",{target:target},(function(data){console.log(data),$(this).blur()}))})),setInterval((function(){$.get("/api/scanner/status",(function(data){let state="idle";data.ocr&&data.waiting&&!data.scan?state="ocr":data.scan&&data.waiting?state="scan":data.scan?state="scan":data.ocr&&!data.scan?state="ocr":data.ocr||data.scan||!data.waiting?data.ocr||data.scan||data.waiting||(state="idle"):state="waiting",set_state(state)}))}),1e3),$("#triggerFiles").on("click",(function(e){var offcanvas;e.preventDefault(),load_files_offcanvas(),new bootstrap.Offcanvas($("#offcanvasFiles")[0]).show()}))})),$(document).on("click",".refresh-files",(function(e){e.preventDefault(),load_files_offcanvas()})),$(document).on("click",".file-rename",(function(e){e.preventDefault(),toggle_file_rename($(this))})),$(document).on("click",".file-rename-confirm",(function(e){e.preventDefault(),url=$(this).attr("href");var parentDiv,parentId=$(this).closest(".list-group-item").attr("id"),filename=$("#"+parentId+" .file-name-original").val(),new_filename=$("#"+parentId+" .file-name-new").val(),new_filename_prefix="none";$("#"+parentId+" .file-rename-prefix-none").is(":checked")&&(new_filename_prefix="none"),$("#"+parentId+" .file-rename-prefix-date").is(":checked")&&(new_filename_prefix="date"),$("#"+parentId+" .file-rename-prefix-datetime").is(":checked")&&(new_filename_prefix="datetime"),console.log("url: "+url),console.log("filename: "+filename),console.log("new_filename: "+new_filename),console.log("new_filename_prefix: "+new_filename_prefix),""==new_filename&&"none"==new_filename_prefix?alert("Please enter a new filename or select a prefix"):(payload={new_filename:new_filename,new_filename_prefix:new_filename_prefix},console.log(payload),$.ajax({url:url,type:"PUT",contentType:"application/json",data:JSON.stringify(payload),success:function(response){console.log(response),console.log("File renamed"),toggle_file_rename($(this)),load_files_offcanvas()},error:function(xhr,status,error){console.log("File NOT renamed"),toggle_file_error(parentId,"Rename failed")}}))})),$(document).on("click",".file-rename-cancel",(function(e){e.preventDefault(),toggle_file_rename($(this))})),$(document).on("click",".file-delete",(function(e){e.preventDefault(),toggle_file_delete($(this))})),$(document).on("click",".file-delete-confirm",(function(e){e.preventDefault();var parentDiv,parentId=$(this).closest(".list-group-item").attr("id");url=$(this).attr("href"),$.ajax({url:url,type:"DELETE",success:function(response){toggle_file_success(parentId,"File deleted"),setTimeout(()=>{load_files_offcanvas()},500)},error:function(response,xhr,status,error){toggle_file_error(parentId,"Delete failed")}})})),$(document).on("click",".file-delete-cancel",(function(e){e.preventDefault(),toggle_file_delete($(this))})); \ No newline at end of file diff --git a/www/html/assets/style.css b/www/html/assets/style.css new file mode 100755 index 0000000..4bb2206 --- /dev/null +++ b/www/html/assets/style.css @@ -0,0 +1,16 @@ +.list-group-hover, +.list-group-item:hover { + background-color: #f5f5f5; +} + +.file-name-new{ + font-weight:bolder; + font-size: 1rem; +} + +#fileslist :focus, +#fileslist :active { + box-shadow: none; + outline: none; +} + diff --git a/www/html/assets/style.min.css b/www/html/assets/style.min.css new file mode 100755 index 0000000..f494bd9 --- /dev/null +++ b/www/html/assets/style.min.css @@ -0,0 +1 @@ +.list-group-hover,.list-group-item:hover{background-color:#f5f5f5}.file-name-new{font-weight:bolder;font-size:1rem}#fileslist :active,#fileslist :focus{box-shadow:none;outline:0} \ No newline at end of file diff --git a/www/html/index.php b/www/html/index.php new file mode 100644 index 0000000..1cb3507 --- /dev/null +++ b/www/html/index.php @@ -0,0 +1,130 @@ + "Warning", + E_NOTICE => "Notice", + E_ERROR => "Error", + E_API => "API", // Custom log type + E_FRONTEND => "Frontend", // Custom log type + default => "Unknown" + }; + + if (($errno == E_API) OR ($errno == E_FRONTEND)) { + $logMessage = "[$errorType] $errstr\n"; + } else { + $logMessage = "[$date] [$errorType] $errstr in $errfile on line $errline\n"; + } + + error_log($logMessage, 3, '/var/log/scanner.log'); +} + +set_error_handler("customErrorHandler"); +set_include_path('/var/www/private/'); + +include('config.php'); +require_once('classes/AltoRouter.php'); +require_once('helper.php'); + +if (!isset($TZ)) { + $TZ = 'Europe/Berlin'; +} +date_default_timezone_set($TZ); + +#session_start(); + +$router = new AltoRouter(); +$router->addMatchTypes(array('char' => '(?:[^\/]*)')); + + +// Frontend routes + +$router->map( 'GET', '/', function() { + require_once 'views/frontend/home.php'; +}); + + +$router->map( 'GET', '/list-files', function() { + require_once 'views/frontend/file-list.php'; +}); + + +$router->map( 'GET', '/file/[char:file]/rename', function( $file) { + require_once 'views/frontend/file-rename.php'; +}); + + +$router->map( 'GET', '/file/[char:file]/delete', function( $file) { + require_once 'views/frontend/file-delete.php'; +}); + + +// API routes + +$router->map( 'GET', '/api/scanner/status', function() { + require_once 'views/api/scanner-status.php'; +}); + + +$router->map( 'POST', '/api/scanner/scanto', function() { + $scanto = $_POST["target"]; + $method = 'return'; + require_once 'views/api/scanner-scanto.php'; +}); + + +$router->map( 'GET', '/api/scanner/scanto/[char:parameter]', function( $parameter) { + $scanto = $parameter; + $method = 'wait'; + require_once 'views/api/scanner-scanto.php'; +}); + + +$router->map( 'GET', '/api/file-list', function() { + require_once 'views/api/file-list.php'; +}); + +$router->map( 'GET', '/api/file/[char:file]/info', function( $file) { + require_once 'views/api/file-info.php'; +}); + + +$router->map( 'GET', '/api/file/[char:file]/download', function( $file) { + require_once 'views/api/file-download.php'; +}); + +$router->map( 'DELETE', '/api/file/[char:file]/delete', function( $file) { + require_once 'views/api/file-delete.php'; +}); + + +$router->map( 'PUT', '/api/file/[char:file]/rename', function( $file) { + require_once 'views/api/file-rename.php'; +}); + + +$router->map( 'GET', '/api/dev/timezone', function() { + require_once 'views/api/dev-timezone.php'; +}); + + +$match = $router->match(); + + +// Call closure or throw 404 status if route not found + +if( is_array($match) && is_callable( $match['target'] ) ) { + call_user_func_array( $match['target'], $match['params'] ); +} else { + if (str_contains($_SERVER['REQUEST_URI'], '/api')) { + send_json_error(404, 'Not Found'); + } else { + send_error_page(404, $page_title='404', $page_message='Sorry, the page you are looking for could not be found.'); + } + exit(); +} +?> \ No newline at end of file diff --git a/www/private/classes/AltoRouter.php b/www/private/classes/AltoRouter.php new file mode 100755 index 0000000..daeab62 --- /dev/null +++ b/www/private/classes/AltoRouter.php @@ -0,0 +1,300 @@ + + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +class AltoRouter +{ + + /** + * @var array Array of all routes (incl. named routes). + */ + protected $routes = []; + + /** + * @var array Array of all named routes. + */ + protected $namedRoutes = []; + + /** + * @var string Can be used to ignore leading part of the Request URL (if main file lives in subdirectory of host) + */ + protected $basePath = ''; + + /** + * @var array Array of default match types (regex helpers) + */ + protected $matchTypes = [ + 'i' => '[0-9]++', + 'a' => '[0-9A-Za-z]++', + 'h' => '[0-9A-Fa-f]++', + '*' => '.+?', + '**' => '.++', + '' => '[^/\.]++' + ]; + + /** + * Create router in one call from config. + * + * @param array $routes + * @param string $basePath + * @param array $matchTypes + * @throws Exception + */ + public function __construct(array $routes = [], string $basePath = '', array $matchTypes = []) + { + $this->addRoutes($routes); + $this->setBasePath($basePath); + $this->addMatchTypes($matchTypes); + } + + /** + * Retrieves all routes. + * Useful if you want to process or display routes. + * @return array All routes. + */ + public function getRoutes(): array + { + return $this->routes; + } + + /** + * Add multiple routes at once from array in the following format: + * + * $routes = [ + * [$method, $route, $target, $name] + * ]; + * + * @param array $routes + * @return void + * @author Koen Punt + * @throws Exception + */ + public function addRoutes($routes) + { + if (!is_array($routes) && !$routes instanceof Traversable) { + throw new RuntimeException('Routes should be an array or an instance of Traversable'); + } + foreach ($routes as $route) { + call_user_func_array([$this, 'map'], $route); + } + } + + /** + * Set the base path. + * Useful if you are running your application from a subdirectory. + * @param string $basePath + */ + public function setBasePath(string $basePath) + { + $this->basePath = $basePath; + } + + /** + * Add named match types. It uses array_merge so keys can be overwritten. + * + * @param array $matchTypes The key is the name and the value is the regex. + */ + public function addMatchTypes(array $matchTypes) + { + $this->matchTypes = array_merge($this->matchTypes, $matchTypes); + } + + /** + * Map a route to a target + * + * @param string $method One of 5 HTTP Methods, or a pipe-separated list of multiple HTTP Methods (GET|POST|PATCH|PUT|DELETE) + * @param string $route The route regex, custom regex must start with an @. You can use multiple pre-set regex filters, like [i:id] + * @param mixed $target The target where this route should point to. Can be anything. + * @param string $name Optional name of this route. Supply if you want to reverse route this url in your application. + * @throws Exception + */ + public function map(string $method, string $route, $target, string $name = null) + { + + $this->routes[] = [$method, $route, $target, $name]; + + if ($name) { + if (isset($this->namedRoutes[$name])) { + throw new RuntimeException("Can not redeclare route '{$name}'"); + } + $this->namedRoutes[$name] = $route; + } + } + + /** + * Reversed routing + * + * Generate the URL for a named route. Replace regexes with supplied parameters + * + * @param string $routeName The name of the route. + * @param array $params Associative array of parameters to replace placeholders with. + * @return string The URL of the route with named parameters in place. + * @throws Exception + */ + public function generate(string $routeName, array $params = []): string + { + + // Check if named route exists + if (!isset($this->namedRoutes[$routeName])) { + throw new RuntimeException("Route '{$routeName}' does not exist."); + } + + // Replace named parameters + $route = $this->namedRoutes[$routeName]; + + // prepend base path to route url again + $url = $this->basePath . $route; + + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + foreach ($matches as $index => $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if ($pre) { + $block = substr($block, 1); + } + + if (isset($params[$param])) { + // Part is found, replace for param value + $url = str_replace($block, $params[$param], $url); + } elseif ($optional && $index !== 0) { + // Only strip preceding slash if it's not at the base + $url = str_replace($pre . $block, '', $url); + } else { + // Strip match block + $url = str_replace($block, '', $url); + } + } + } + + return $url; + } + + /** + * Match a given Request Url against stored routes + * @param string $requestUrl + * @param string $requestMethod + * @return array|boolean Array with route information on success, false on failure (no match). + */ + public function match(string $requestUrl = null, string $requestMethod = null) + { + + $params = []; + + // set Request Url if it isn't passed as parameter + if ($requestUrl === null) { + $requestUrl = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; + } + + // strip base path from request url + $requestUrl = substr($requestUrl, strlen($this->basePath)); + + // Strip query string (?a=b) from Request Url + if (($strpos = strpos($requestUrl, '?')) !== false) { + $requestUrl = substr($requestUrl, 0, $strpos); + } + + $lastRequestUrlChar = $requestUrl ? $requestUrl[strlen($requestUrl)-1] : ''; + + // set Request Method if it isn't passed as a parameter + if ($requestMethod === null) { + $requestMethod = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + } + + foreach ($this->routes as $handler) { + list($methods, $route, $target, $name) = $handler; + + $method_match = (stripos($methods, $requestMethod) !== false); + + // Method did not match, continue to next route. + if (!$method_match) { + continue; + } + + if ($route === '*') { + // * wildcard (matches all) + $match = true; + } elseif (isset($route[0]) && $route[0] === '@') { + // @ regex delimiter + $pattern = '`' . substr($route, 1) . '`u'; + $match = preg_match($pattern, $requestUrl, $params) === 1; + } elseif (($position = strpos($route, '[')) === false) { + // No params in url, do string comparison + $match = strcmp($requestUrl, $route) === 0; + } else { + // Compare longest non-param string with url before moving on to regex + // Check if last character before param is a slash, because it could be optional if param is optional too (see https://github.com/dannyvankooten/AltoRouter/issues/241) + if (strncmp($requestUrl, $route, $position) !== 0 && ($lastRequestUrlChar === '/' || $route[$position-1] !== '/')) { + continue; + } + + $regex = $this->compileRoute($route); + $match = preg_match($regex, $requestUrl, $params) === 1; + } + + if ($match) { + if ($params) { + foreach ($params as $key => $value) { + if (is_numeric($key)) { + unset($params[$key]); + } + } + } + + return [ + 'target' => $target, + 'params' => $params, + 'name' => $name + ]; + } + } + + return false; + } + + /** + * Compile the regex for a given route (EXPENSIVE) + * @param string $route + * @return string + */ + protected function compileRoute(string $route): string + { + if (preg_match_all('`(/|\.|)\[([^:\]]*+)(?::([^:\]]*+))?\](\?|)`', $route, $matches, PREG_SET_ORDER)) { + $matchTypes = $this->matchTypes; + foreach ($matches as $match) { + list($block, $pre, $type, $param, $optional) = $match; + + if (isset($matchTypes[$type])) { + $type = $matchTypes[$type]; + } + if ($pre === '.') { + $pre = '\.'; + } + + $optional = $optional !== '' ? '?' : null; + + //Older versions of PCRE require the 'P' in (?P) + $pattern = '(?:' + . ($pre !== '' ? $pre : null) + . '(' + . ($param !== '' ? "?P<$param>" : null) + . $type + . ')' + . $optional + . ')' + . $optional; + + $route = str_replace($block, $pattern, $route); + } + } + return "`^$route$`u"; + } +} \ No newline at end of file diff --git a/www/private/helper.php b/www/private/helper.php new file mode 100755 index 0000000..133e644 --- /dev/null +++ b/www/private/helper.php @@ -0,0 +1,161 @@ + $http_code, 'message' => $message)); + die(); +} + + +function send_error_page($http_code, $page_title='', $page_message=''){ + http_response_code($http_code); + if ($page_title != '' && $page_message != ''){ + require 'views/frontend/error.php'; + } + die(); +} + +/** + * Constructs a safe file path within a specified directory. + * + * This function takes a directory and a filename, constructs the full file path, + * and ensures that the file path is within the specified directory. It prevents + * directory traversal attacks by validating the real path of the constructed file path. + * + * @param string $directory The directory in which the file should be located. + * @param string $filename The name of the file. + * @return string|false The real path to the file if it is within the specified directory, or false if it is not. + */ +function file_get_real_filepath($directory, $filename) { + + $filename = basename($filename); + $filePath = $directory . DIRECTORY_SEPARATOR . $filename; + $realPath = realpath($filePath); + + if ($realPath === false || strpos($realPath, realpath($directory)) !== 0) { + return false; + } + + return $realPath; +} + + +function file_get_verified_fileinfo($dir, $file) { + + + $filename = file_get_real_filepath($dir, $file); + + if ($filename === false) { + send_json_error(400, "No valid file specified"); + } + + if(!file_exists($filename)){ + send_json_error(404, "File does not exist"); + } + + $pathInfo = pathinfo($filename); + $filenameWithoutExtension = pathinfo($filename, PATHINFO_FILENAME); + $fileCreationTime = filectime($filename); + $fileModificationTime = filemtime($filename); + $finfo = new finfo(FILEINFO_MIME_TYPE); + $mimetype = $finfo->file($filename); + + $file_info = array( + 'full_path' => $filename, + 'file' => $pathInfo['basename'] ?? '', + 'name' => $filenameWithoutExtension ?? '', + 'name_clean' => '', + 'dir' => $pathInfo['dirname'] ?? '', + 'date_from_name' => '', + 'time_from_name' => '', + 'fileCreationTime' => $fileCreationTime, + 'fileModificationTime' => $fileModificationTime, + 'date_from_file' => date('Y-m-d', $fileModificationTime), + 'time_from_file' => date('H-i-s', $fileModificationTime), + 'extension' => $pathInfo['extension'] ?? '', + 'mimetype' => $mimetype, + 'size' => filesize($filename) + ); + + if (preg_match('/(\d{4}-\d{2}-\d{2})(?:-(\d{2})(?:-(\d{2})(?:-(\d{2}))?)?)?/', $filename, $matches)) { + + $file_info['date_from_name'] = $matches[1] ?? ''; + + if (isset($matches[2])) { + $file_info['time_from_name'] = $matches[2] . ':' . ($matches[3] ?? '00') . ':' . ($matches[4] ?? '00'); + } + } + // Combine date and time with the dash to form the full datetime string + $pattern = '/^\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2}\s*/'; + $remove_datetime = $file_info['date_from_name'].'-'.$file_info['time_from_name']; + $clean_name = preg_replace($pattern, '', $filenameWithoutExtension); + + // Remove only the date and time without extra spaces around + $remove_date = $file_info['date_from_name']; + $clean_name = str_replace($remove_date, '', $clean_name); + + $remove_time = $file_info['time_from_name']; + $clean_name = str_replace($remove_time, '', $clean_name); + + // Trim any remaining leading or trailing spaces + $clean_name = trim($clean_name); + $file_info['name_clean'] = $clean_name; + + if($file_info['mimetype'] != 'application/pdf'){ + send_json_error(400, "No valid file specified"); + } + + + return $file_info; +} + + +function file_is_valid_name_string($filename) { + + $pattern = '/[<>:"\/\\|?*\x00-\x1F]/'; // Invalid characters for filenames + + if (preg_match($pattern, $filename)) { + return false; + } + + if (strlen($filename) > 255) { + return false; + } + + if (strlen($filename) < 3) { + return false; + } + + + return true; +} + +function list_files($dir){ + $files = scandir($dir); + $files = array_diff($files, array('.', '..')); + + $data = array(); + foreach ($files as $file) { + $filePath = $dir . '/' . $file; + if (is_file($filePath) && pathinfo($filePath, PATHINFO_EXTENSION) === 'pdf') { + $data[] = file_get_verified_fileinfo($dir, $file); + } + + } + uasort($data, function($a, $b) { + return $b['fileModificationTime'] <=> $a['fileModificationTime']; + }); + return array_values($data); +} +?> \ No newline at end of file diff --git a/www/private/views/api/dev-timezone.php b/www/private/views/api/dev-timezone.php new file mode 100755 index 0000000..a812b54 --- /dev/null +++ b/www/private/views/api/dev-timezone.php @@ -0,0 +1,12 @@ + date_default_timezone_get(), + 'datetime' => date("Y-m-d H:i:s") +); +trigger_error("Timezone: ".$timezone_data['timezone'] . " DateTime: ".$timezone_data['datetime'], E_API); +json($timezone_data); + +?> \ No newline at end of file diff --git a/www/private/views/api/file-delete.php b/www/private/views/api/file-delete.php new file mode 100755 index 0000000..06e8a3a --- /dev/null +++ b/www/private/views/api/file-delete.php @@ -0,0 +1,26 @@ + 'success')); +} else { + trigger_error("can not deleted file ".$file_info['full_path'], E_API); + send_json_error(500, "Could not delete file"); +} + +?> \ No newline at end of file diff --git a/www/private/views/api/file-download.php b/www/private/views/api/file-download.php new file mode 100644 index 0000000..f831d08 --- /dev/null +++ b/www/private/views/api/file-download.php @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/www/private/views/api/file-info.php b/www/private/views/api/file-info.php new file mode 100755 index 0000000..8b5c952 --- /dev/null +++ b/www/private/views/api/file-info.php @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/www/private/views/api/file-list.php b/www/private/views/api/file-list.php new file mode 100755 index 0000000..1a1e782 --- /dev/null +++ b/www/private/views/api/file-list.php @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/www/private/views/api/file-rename.php b/www/private/views/api/file-rename.php new file mode 100755 index 0000000..a7b6049 --- /dev/null +++ b/www/private/views/api/file-rename.php @@ -0,0 +1,81 @@ + fileatime($original_filename), // Access time + 'modification_time' => filemtime($original_filename) // Modification time + ]; +} + +$file_info = file_get_verified_fileinfo('/scans/', urldecode($file)); + +$original_filename = $file_info['full_path']; + +$jsonData = file_get_contents('php://input'); +$data = json_decode($jsonData, true); + + + +if ($data !== null) { + $new_filename = $data['new_filename']; + $new_filename_prefix = $data['new_filename_prefix']; + + +} else { + trigger_error("JSON decoding error", E_API); + send_json_error(400, "JSON decoding error"); +} + +if (file_is_valid_name_string($new_filename)){ + + + $target_filename = preg_replace('/[^a-zA-Z0-9äöüßÄÖÜ\-\_ ]/u', '', $new_filename) . '.' . strtolower($file_info['extension']); + + $final_filename = $file_info['dir'] . '/' . $target_filename; + + + if ($new_filename_prefix == 'date') { + + $final_filename = $file_info['dir'] . '/' . $file_info['date_from_file'] . ' ' . $target_filename; + + } elseif ($new_filename_prefix == 'datetime') { + $final_filename = $file_info['dir'] . '/' . $file_info['date_from_file'] . '-' . $file_info['time_from_file'] . ' ' . $target_filename; + } + + + // Get access and modification times of the old file + $times = getFileTimes($original_filename); + + // Rename the file + if (rename($original_filename, $final_filename)) { + + // Restore access and modification time using 'touch' + @touch($final_filename, $times['modification_time'], $times['access_time']); + send_json_error(200, "Renamed file successfully"); + } else { + trigger_error("Error renaming the file", E_API); + send_json_error(400, "Error renaming the file"); + } + +}else{ + send_json_error(400, "Invalid filename"); +} + + + +?> \ No newline at end of file diff --git a/www/private/views/api/scanner-scanto.php b/www/private/views/api/scanner-scanto.php new file mode 100644 index 0000000..a1cfa19 --- /dev/null +++ b/www/private/views/api/scanner-scanto.php @@ -0,0 +1,58 @@ + 'Scan triggered','method' => 'post','target' => $target)); + } else if ($method == 'wait') { + shell_exec('sudo -u \#'.$UID.' /opt/brother/scanner/brscan-skey/script/scanto'.$target.'.py'); + json(array('message' => 'Scan triggered','method' => 'get','target' => $target)); + } +} + + +$target = safe_guard_target($scanto); + +trigger_script($target, $UID, $method); + +?> \ No newline at end of file diff --git a/www/private/views/api/scanner-status.php b/www/private/views/api/scanner-status.php new file mode 100644 index 0000000..90bad2c --- /dev/null +++ b/www/private/views/api/scanner-status.php @@ -0,0 +1,49 @@ + 180) { + // File is stale, remove it + unlink($statusFile); + return false; + } + + return true; +} + +// Check if the scanimage, sleep, and curl processes are running +$result = array( + 'scan' => isProcessRunning('scanimage'), + 'waiting' => isWaitingStatus(), + 'ocr' => isProcessRunning('curl') +); + +// Output the result as JSON +json($result); + +?> diff --git a/www/private/views/frontend/common-head.php b/www/private/views/frontend/common-head.php new file mode 100755 index 0000000..0fa1695 --- /dev/null +++ b/www/private/views/frontend/common-head.php @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/www/private/views/frontend/common-javascript.php b/www/private/views/frontend/common-javascript.php new file mode 100755 index 0000000..164d00f --- /dev/null +++ b/www/private/views/frontend/common-javascript.php @@ -0,0 +1,2 @@ + + diff --git a/www/private/views/frontend/error.php b/www/private/views/frontend/error.php new file mode 100755 index 0000000..c02d8b2 --- /dev/null +++ b/www/private/views/frontend/error.php @@ -0,0 +1,45 @@ + + + + + + + <?php echo($page_title); ?> + + + + + +
+
+
+
+ +

+

+ + +
+
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/www/private/views/frontend/file-list.php b/www/private/views/frontend/file-list.php new file mode 100644 index 0000000..4f8a6af --- /dev/null +++ b/www/private/views/frontend/file-list.php @@ -0,0 +1,89 @@ + +
+ +
+ + +
+
+ + + + +
+
+
KB
+
Are you sure you want to delete this file?
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+
+
+ + + + + + + +
+
+
+
+ + DELETE +
+
+
+
+ + SAVE +
+
+
+
+ + + +
+
+ + diff --git a/www/private/views/frontend/home.php b/www/private/views/frontend/home.php new file mode 100755 index 0000000..8135db6 --- /dev/null +++ b/www/private/views/frontend/home.php @@ -0,0 +1,113 @@ + + + + + + + Brother <?php echo($MODEL); ?> + + + + + + +
+
+
+
+ +

+

Ready to scan

+ + '.$button_file.'

'); + } + if (!isset($DISABLE_GUI_SCANTOEMAIL) || $DISABLE_GUI_SCANTOEMAIL != true) { + echo('

'.$button_email.'

'); + } + if (!isset($DISABLE_GUI_SCANTOIMAGE) || $DISABLE_GUI_SCANTOIMAGE != true) { + echo('

'.$button_image.'

'); + } + if (!isset($DISABLE_GUI_SCANTOOCR) || $DISABLE_GUI_SCANTOOCR != true) { + echo('

'.$button_ocr.'

'); + } + ?> +
+
+
+
+ + +
+ +
+
Last scanned
+ +
+
+ + + + + +
+
+ + + + + + + + + + + + + + + \ No newline at end of file