Note
Montscan is not fully production-ready and is currently in active development but is fairly usable.
It achieve 97.5% success rate on a small test set of 1000 documents.
Montscan: Automated scanner document processor with Vision AI, AI naming, and flexible upload!
โญ If you find Montscan useful, please consider giving it a star it really helps the project grow!
- ๐ก FTP Server - Receives documents from network scanners via FTP
- ๐ SFTP Server - Receives documents over SSH file transfer
- ๐ค SCP Server - Receives documents via the legacy SCP protocol
- ๐ฅ Samba Server - Polls a remote SMB share for incoming PDFs
- ๐ Folder Watcher - Monitors a local directory; renames in place or moves to an output folder
- ๐๏ธ Vision AI Processing - Analyzes scanned documents using Ollama vision models
- ๐ค AI-Powered Naming - Generates descriptive, date-aware filenames via Ollama
- โ๏ธ WebDAV Integration - Uploads processed documents to Nextcloud, ownCloud, and any WebDAV server
- ๐๏ธ Samba Provider - Delivers processed documents to an SMB/CIFS network share
- ๐ณ Docker Support - Easy deployment with Docker Compose
Montscan was originally written in Go. Starting with v2.0.0 it has been rewritten from the ground up in Rust. The goal was a single self-contained binary, cleaner boundaries between components, and room to add new ways for scanners to send documents.
- No more external PDF tools - The Go version shelled out to Poppler (
pdftoppm) or ImageMagick to turn PDFs into images. Pages are now rendered in-process withhayro, so nothing beyond themontscanbinary is needed at runtime. - New ingresses - Montscan can now receive documents over SFTP and the legacy SCP protocol, alongside FTP, Samba polling and the folder watcher.
- Pluggable architecture - Every ingress implements a common
Ingresstrait and only hands finished PDFs to the document processor. Delivery (WebDAV, Samba, local move) lives in a separateegressmodule. Adding a new protocol no longer touches the AI or upload code. See Architecture. - Better FTP behind NAT/Docker - The passive port range and advertised address can now be configured (
FTP_PASSIVE_PORTS,FTP_PASSIVE_HOST). - Ollama timeout -
OLLAMA_TIMEOUTsets how long Montscan waits for a filename before falling back toscan_<timestamp>.pdf. This is useful for slower "thinking" vision models. - Smaller Docker image - The runtime image no longer installs
poppler-utils.
Warning
v2.0.0 is a breaking release and is not backward compatible with 1.x. Read the notes below before upgrading.
The Go implementation (1.x) is kept on the legacy-v1.0.0 branch and no longer receives updates.
- Environment variables keep their names. The legacy
SAMBA_INGRESS_*names are still accepted as fallbacks forSAMBA_SERVER_*. - Name collisions - Uploads to WebDAV and Samba, and files received over FTP, replace an existing file with the same name. 1.x appended
_1,_2, โฆ instead. Local moves by the folder watcher still never overwrite. - Runtime files moved to
storage/- Received scans now default to./storage/inbox(previously./scans), and SSH host keys live in./storage/ssh. If you relied on the old default, setFTP_UPLOAD_DIR=./scansor move the folder. - Building from source now requires Rust 1.92+ instead of Go (
cargo build --release). - Docker - Pull the new image (
docker-compose pull), or rebuild it if you build from source. If you enable SFTP or SCP, publish ports2222/2223and mount./storage:/app/storageso received scans and the generated SSH host key survive container recreation. The provideddocker-compose.ymlalready does both. - Logging now uses
RUST_LOG(defaultinfo).
AI tools helped during the rewrite, mainly to organize the work into commits and to audit the code for bugs and edge cases. Every change was reviewed before it was merged.
- Rust Rewrite
- Architecture
- Prerequisites
- Installation
- Configuration
- Usage
- Docker Deployment
- Troubleshooting
- License
Montscan is written entirely in Rust, with no external programs or C libraries required at runtime (PDF pages are rendered in-process). It is a single crate; all code lives in src/. Each ingress is a module under src/ingress/, built on an existing protocol library, and implements the Ingress trait. Ingresses only hand finished PDFs to a DocumentProcessor; they know nothing about AI naming or delivery.
| Module | Role | Built on |
|---|---|---|
ingress |
Ingress / DocumentProcessor traits |
โ |
ingress::ftp |
FTP ingress | libunftp + unftp-sbe-fs |
ingress::sftp |
SFTP ingress | russh + russh-sftp |
ingress::scp |
SCP ingress | russh (no SCP server crate exists; the small SCP sink protocol is implemented in the module) |
ingress::smb |
SMB share poller | smb (pure Rust SMB2/3 client) |
ingress::folder |
Local folder watcher | tokio |
ingress::ssh |
Host key, auth and path jailing shared by SFTP and SCP | russh |
agent |
PDF rendering and Ollama filename generation | hayro (pure Rust PDF renderer), reqwest, image |
egress |
WebDAV / Samba / local delivery | reqwest, smb |
config, env, banner |
Settings and startup banner | โ |
- Rust 1.92+ (to build from source)
- Ollama - Installation guide with a vision model (e.g.,
llava,llama3.2-vision) - WebDAV server or SMB share (optional) - For processed file delivery
-
Clone the repository
git clone https://github.com/sh4den/Montscan.git cd Montscan -
Build the application
cargo build --release # binary: target/release/montscan -
Set up Ollama with a vision model
# Install Ollama from https://ollama.ai/ ollama pull llava # or any other vision-capable model
At least one ingress must be enabled. At least one egress provider should be configured, or the folder watcher can act as both (rename/move in place). Egress is chosen in priority order: WebDAV, then Samba, then a local move when the folder watcher is enabled. With no egress at all, received files are left where they landed.
| Variable | Description | Default |
|---|---|---|
FTP_ENABLED |
Enable the FTP ingress server | true |
FTP_HOST |
FTP server bind address | 0.0.0.0 |
FTP_PORT |
FTP server port | 21 |
FTP_USERNAME |
FTP authentication username | scanner |
FTP_PASSWORD |
FTP authentication password | scanner123 |
FTP_UPLOAD_DIR |
Local directory for received files | ./storage/inbox |
FTP_PASSIVE_PORTS |
Passive data port range | 21000-21010 |
FTP_PASSIVE_HOST |
Address advertised for passive connections (set to the host's LAN IP behind Docker/NAT) | (control connection address) |
Files are processed once a STOR completes, or when an upload is renamed to *.pdf.
| Variable | Description | Default |
|---|---|---|
SFTP_ENABLED |
Enable the SFTP ingress server | false |
SFTP_HOST |
Bind address | 0.0.0.0 |
SFTP_PORT |
Port | 2222 |
SFTP_USERNAME |
Username (password auth only; no shell access) | scanner |
SFTP_PASSWORD |
Password | scanner123 |
SFTP_UPLOAD_DIR |
Directory clients see as / |
./storage/inbox |
SFTP_HOST_KEY |
Ed25519 host key, generated on first start | ./storage/ssh/ssh_host_ed25519_key |
Files are processed when the client closes the file, or when an upload is renamed to *.pdf. Modern scp (OpenSSH 9+) uses the SFTP protocol and works against this port.
| Variable | Description | Default |
|---|---|---|
SCP_ENABLED |
Enable the SCP ingress server | false |
SCP_HOST |
Bind address | 0.0.0.0 |
SCP_PORT |
Port | 2223 |
SCP_USERNAME |
Username (password auth only; no shell access) | scanner |
SCP_PASSWORD |
Password | scanner123 |
SCP_UPLOAD_DIR |
Directory clients see as / |
./storage/inbox |
SCP_HOST_KEY |
Ed25519 host key, generated on first start (may be shared with SFTP) | ./storage/ssh/ssh_host_ed25519_key |
This speaks the legacy SCP protocol (scp -t), as used by scanners and scp -O. Uploads only; recursive (-r) uploads are supported.
| Variable | Description | Default |
|---|---|---|
SAMBA_SERVER_ENABLED |
Enable the Samba ingress poller | false |
SAMBA_SERVER_HOST |
SMB server hostname or IP | localhost |
SAMBA_SERVER_PORT |
SMB port | 445 |
SAMBA_SERVER_USERNAME |
SMB username | - |
SAMBA_SERVER_PASSWORD |
SMB password | - |
SAMBA_SERVER_SHARE |
Share name to mount | scans |
SAMBA_SERVER_PATH |
Path within the share to watch | /scans |
SAMBA_SERVER_POLL_INTERVAL_SEC |
Polling interval in seconds | 10 |
SAMBA_SERVER_DELETE_AFTER_READ |
Remove file from share after download | true |
SAMBA_SERVER_WORK_DIR |
Local staging directory | (FTP_UPLOAD_DIR) |
| Variable | Description | Default |
|---|---|---|
FOLDER_ENABLED |
Enable the local folder watcher | false |
FOLDER_INPUT_DIR |
Directory to watch for incoming PDFs | ./input |
FOLDER_OUTPUT_DIR |
Destination for renamed files; omit to rename in place | - |
FOLDER_POLL_INTERVAL_SEC |
Polling interval in seconds | 5 |
| Variable | Description | Default |
|---|---|---|
WEBDAV_ENABLED |
Enable WebDAV upload | false |
WEBDAV_URL |
WebDAV server base URL | - |
WEBDAV_USERNAME |
WebDAV username | - |
WEBDAV_PASSWORD |
WebDAV password | - |
WEBDAV_UPLOAD_PATH |
Remote path for uploaded files | /Documents/Scanned |
WEBDAV_INSECURE |
Skip TLS verification | false |
| Variable | Description | Default |
|---|---|---|
SAMBA_ENABLED |
Enable Samba upload | false |
SAMBA_HOST |
SMB server hostname or IP | localhost |
SAMBA_PORT |
SMB port | 445 |
SAMBA_USERNAME |
SMB username | - |
SAMBA_PASSWORD |
SMB password | - |
SAMBA_SHARE |
Destination share name | scans |
SAMBA_PATH |
Path within the share | scans |
| Variable | Description | Default |
|---|---|---|
OLLAMA_HOST |
Ollama service URL | http://localhost:11434 |
OLLAMA_MODEL |
Vision model to use | qwen3-vl:8b |
OLLAMA_NUM_CTX |
Context window (tokens) requested per call | 8192 |
OLLAMA_TIMEOUT |
Seconds to wait for Ollama before falling back to a timestamped name | 120 |
LANGUAGE |
Language for AI-generated filenames | english |
Scanner โ FTP โ WebDAV
FTP_ENABLED=true
FTP_USERNAME=scanner
FTP_PASSWORD=secret
WEBDAV_ENABLED=true
WEBDAV_URL=https://cloud.example.com/remote.php/webdav
WEBDAV_USERNAME=user
WEBDAV_PASSWORD=secretScanner โ Samba share โ local folder watcher โ output folder
This is the simplest setup when your scanner already drops files onto a Samba share that is mounted locally. Montscan watches the mount, renames each PDF with an AI-generated name, and moves it to an output directory.
FOLDER_ENABLED=true
FOLDER_INPUT_DIR=/mnt/scanner # your Samba mount point
FOLDER_OUTPUT_DIR=/mnt/documents # where renamed files land
FOLDER_POLL_INTERVAL_SEC=5
FTP_ENABLED=falseScanner โ Samba share โ local folder watcher โ rename in place
Same as above but without a separate output directory โ files are renamed where they sit.
FOLDER_ENABLED=true
FOLDER_INPUT_DIR=/mnt/scanner
# FOLDER_OUTPUT_DIR not set โ rename in place
FTP_ENABLED=falsecp .env.example .env
# edit .env with your values
export $(grep -v '^#' .env | xargs)
./target/release/montscanLogging defaults to info; override with RUST_LOG (e.g. RUST_LOG=debug).
-
Update environment variables in
docker-compose.yml -
(Optional) Enable GPU acceleration for Ollama
By default, Ollama runs on CPU. To use an NVIDIA GPU, set in
.env:COMPOSE_PROFILES=gpu OLLAMA_GPU_COUNT=1
Leave
COMPOSE_PROFILES=cpu(or unset) for CPU-only. -
Start the container
By default the prebuilt image from GitHub Container Registry (
ghcr.io/sh4den/montscan) is used:docker-compose up -d
To build from source instead, uncomment the
build: .line indocker-compose.ymland rundocker-compose up -d --build. -
View logs
docker-compose logs -f
-
Stop the container
docker-compose down
docker build -t montscan .
docker run -d \
-p 21:21 \
-p 21000-21010:21000-21010 \
-v ./input:/app/input \
-v ./output:/app/output \
-e FOLDER_ENABLED=true \
-e FOLDER_INPUT_DIR=/app/input \
-e FOLDER_OUTPUT_DIR=/app/output \
-e FTP_ENABLED=false \
-e OLLAMA_HOST=http://host.docker.internal:11434 \
--name montscan \
montscan- Check that port 21 is not blocked by a firewall
- On Windows, allow the application through Windows Defender Firewall
- Passive data connections must reach the container: publish
FTP_PASSIVE_PORTSand setFTP_PASSIVE_HOSTto the Docker host's LAN IP
- OpenSSH 9+
scpuses the SFTP protocol by default. Usescp -Oagainst the SCP port, or point the client at the SFTP port instead
- The host key is generated on first start at
SFTP_HOST_KEY/SCP_HOST_KEY. In Docker, keep it on the./storagevolume so it survives container recreation
- Verify
FOLDER_INPUT_DIRpoints to the correct path inside the container (use volume mounts) - Check that the directory is readable by the process running Montscan
- Increase
FOLDER_POLL_INTERVAL_SECif the scanner writes files slowly
- Verify Ollama is running:
ollama list - Ensure you have a vision-capable model installed (e.g.,
llava,llama3.2-vision) - Montscan falls back to a timestamp filename (
scan_YYYYMMDD_HHMMSS.pdf) on AI failure
- Check credentials and URL
- Ensure the upload path exists on the server
- For Nextcloud: verify WebDAV is enabled and the URL ends with
/remote.php/webdav - Try
WEBDAV_INSECURE=trueif you use a self-signed certificate
- Password-protected (encrypted) PDFs are not supported; disable PDF encryption on the scanner
This project is licensed under the MIT License - see the LICENSE file for details.
- libunftp - Async FTP server library for Rust
- russh and russh-sftp - SSH and SFTP server libraries for Rust
- smb-rs - Pure Rust SMB2/3 client
- Ollama - Local AI vision model runner
- reqwest - HTTP client used for Ollama and WebDAV
For questions or support, please open an issue on GitHub.
