Skip to content

Accept files from stdin, write safe PDFs to stdout - #1528

Open
apyrgio wants to merge 12 commits into
mainfrom
1522-stdin-stdout
Open

Accept files from stdin, write safe PDFs to stdout#1528
apyrgio wants to merge 12 commits into
mainfrom
1522-stdin-stdout

Conversation

@apyrgio

@apyrgio apyrgio commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Dangerzone CLI now accepts documents via stdin and writes safe PDFs to stdout, following standard Unix pipe conventions:

cat file.pdf | dangerzone-cli > safe.pdf
dangerzone-cli - < file.pdf > safe.pdf
dangerzone-cli < file.pdf > safe.pdf

All status output (banner, progress, errors) goes to stderr so stdout stays clean for PDF data. We also take extra care to detect when we can read from stdin and when we can write to stdout, so that we don't mess with the user's TTY.

Fixes #1522
Refs #206

Remove the private _validate_* functions and their public wrapper
functions that existed to work around a Click 7.x bug (issue #206).
Click 7.x is no longer supported, so these wrappers are unnecessary.

Promote the @errors.handle_document_errors-decorated functions directly
to public names.
apyrgio added 5 commits July 21, 2026 12:43
The ASCII art banner was printing to stdout, which would interfere with
piping safe PDF data. Route all banner output to stderr instead, and
update the corresponding test to read from stderr.
All human-readable status output (print_header, settings messages, OCR
language validation, container image warnings, failed document display)
now goes to stderr via click.echo(..., err=True), keeping stdout clean
for potential PDF data output.

Also use str(document) for failed doc display instead of
document.input_filename, which is preparatory for data-based documents
where input_filename would raise.
Extend the Document class to accept raw bytes via a new 'data'
parameter instead of requiring a file path. This is the foundation
for stdin support.

- New 'data' parameter in __init__ (default None, backward-compatible)
- Constructor requires either input_filename or data
- New Document.from_stdin() classmethod reads from sys.stdin.buffer
- New open() method returns BinaryIO (file or BytesIO)
- New write() method writes to stdout or output file
- New sanitized_input_filename property
- announce_id() handles both file and stdin documents
- __eq__, __hash__, __str__ handle data-based documents
- Add DangerzoneCore.add_document_from_stdin()
- Update tests for new constructor requirements
Decouple the isolation provider from file-based I/O by using the
Document abstraction methods:
- document.open() instead of open(document.input_filename, 'rb')
- document.write() instead of safe_doc.save() + os.replace()

This enables the provider to work with data-based documents (stdin)
without modification.
Implement stdin/stdout I/O for the Dangerzone CLI.

- Add _initialize_documents() handling stdin detection, validation,
  and document creation
- Accept '-' as a filename to explicitly read from stdin
- Accept implicit stdin when no filenames given and stdin is piped
- Write safe PDF to stdout when no --output-filename is specified
- Refuse to write to a terminal without --output-filename
- Reject --archive with stdin input
- Update @click.command() help text mentioning stdin
- Add STDIO_DESCRIPTOR constant and skip validation for '-'
- Add TestCliIO test class covering all stdin/stdout scenarios

Fixes #1522
@apyrgio
apyrgio force-pushed the 1522-stdin-stdout branch from 5d37a5a to d5e3c95 Compare July 21, 2026 09:43

@almet almet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Amazing, thanks :-) I've stumbled onto this need in the past when building a web interface on top of DZ, and I can clearly see how that can be useful in general.

I'm requesting some changes on this PR, but it's headed the right direction! Thanks.

safe_doc.save(document.sanitized_output_filename)
os.replace(document.sanitized_output_filename, document.output_filename)
# Write the safe PDF to the document's destination.
document.write(safe_doc.tobytes())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this changes the default CLI user experience?

I believe we should output the files somewhere the user has specified, and if nothing was specified, error out and ask where this should be.

Presenting the user with the contents of the file should be done only if it was asked explicitely, e.g. by setting input and output to - (to mean it's should be read and written to a STDIN and STDOUT)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've made the input from stdin and output to stdout a conscious user choice in 6b065a6. This means that:

dangerzone-cli < file.pdf > safe.pdf

becomes:

dangerzone-cli - -o - < file.pdf > safe.pdf

which is more dash-y, but ultimately fine.

Note that with the above change, we no longer check for TTYs, since this no longer happens implicitly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you'd like to get a less dashy experience, it's also possible to have a --stdio argument, for instance.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, people may be more familiar with the dashes than with a custom --stdio flag, so I'm ultimately ok with it.

Comment thread dangerzone/cli.py Outdated
Comment thread dangerzone/cli.py Outdated
"Accepts file paths as arguments, or reads from stdin when no\n"
"files are given (or when '{args.STDIO_DESCRIPTOR}' is passed as a filename).\n"
"When reading from stdin, the safe PDF is written to stdout unless\n"
"--output-filename is specified. All status output goes to stderr."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be added in the docstring instead? (Click picks it up)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done on purpose actually (also, I think Click shows the docstring as a help message as well). I wanted a quick way to explain how to use dangerzone-cli, for those who encounter it for the first time. To better explain my end goal, I've rephrased and added extra stuff in 3391a26. It now looks like this:

[user@dangerzone-dev dangerzone]$ poetry run dangerzone-cli --help
Usage: dangerzone-cli [OPTIONS] [FILENAMES]...

  Convert potentially dangerous documents to safe PDFs.

  Documents are converted inside a sandbox, so that any embedded threats are
  neutralized. You can also use OCR to add a searchable text layer to the safe
  PDF, via the --ocr-lang option.

  Pass one or more file paths as arguments, or use '-' to read a document from
  standard input. For single-document conversions, you can write the safe PDF
  to a file with -o <file>, or to standard output with -o -.

  Examples:

      dangerzone-cli evidence.odt                      # create evidence-safe.pdf
      dangerzone-cli doc1.pdf doc2.docx                # create doc1-safe.pdf and doc2-safe.pdf
      dangerzone-cli --archive report.docx             # create report-safe.pdf and move the original under 'unsafe/'
      dangerzone-cli --ocr-lang eng scan.pdf           # add a searchable text layer
      dangerzone-cli -o safe.pdf evidence.odt          # create safe.pdf
      dangerzone-cli -o - evidence.odt > safe.pdf      # write the safe PDF to stdout
      cat evidence.pdf | dangerzone-cli - -o safe.pdf  # read from stdin and write to safe.pdf
      cat in.pdf | dangerzone-cli - -o - > out.pdf     # read from stdin to write to stdout

It's probably a little verbose and I can drop some examples, but I mostly want to know if you find this valuable.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify why using docstrings didn't work? From my perspective, it looks like a way to handle the spacing and returns automatically, but I might be missing something.

Otherwise, I really like the idea of having documentation in the CLI like that, with examples it's great :-)

The example that reads "# write the safe PDF to stdout" should probably be renamed to "use unix pipes to convert to safe.pdf", or we should remove the last >.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify why using docstrings didn't work? From my perspective, it looks like a way to handle the spacing and returns automatically, but I might be missing something.

Oh I'm sorry! When I read your initial message, I thought that you didn't want the examples in the help message, and wanted to move them in the docstring (hence my comment).

I agree, I've moved them to the docstring and tweaked a bit the sentence you pointed out, as well as the others. See a1a8d3e.

Comment thread dangerzone/cli.py Outdated
Comment thread dangerzone/document.py Outdated
Comment thread dangerzone/logic.py Outdated
@apyrgio
apyrgio force-pushed the 1522-stdin-stdout branch from 0cad265 to 91eb8e4 Compare August 12, 2026 11:58
@apyrgio
apyrgio force-pushed the 1522-stdin-stdout branch from 91eb8e4 to a1a8d3e Compare August 12, 2026 12:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Accept files from stdin

2 participants