Skip to content

Add setup script and README to configure Brother HL-L1222(V) print server on Raspberry Pi (openSUSE)#32

Open
ib-bsb-br wants to merge 1 commit into
mainfrom
codex/create-tutorial-for-brother-hl-l1222-v-setup-on-raspberry-pi-qkxjy4
Open

Add setup script and README to configure Brother HL-L1222(V) print server on Raspberry Pi (openSUSE)#32
ib-bsb-br wants to merge 1 commit into
mainfrom
codex/create-tutorial-for-brother-hl-l1222-v-setup-on-raspberry-pi-qkxjy4

Conversation

@ib-bsb-br

@ib-bsb-br ib-bsb-br commented Apr 29, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Provide a one-step setup to configure a Raspberry Pi 4B+ running openSUSE Tumbleweed as a CUPS/IPPoE print server for a USB-connected Brother HL-L1222/HL-L1222-V so Android devices can print over the same Wi‑Fi network.
  • Make IPP/Mopria-compatible Wi‑Fi printing the primary, supported workflow while offering optional Bluetooth and LAN upload fallbacks for experimentation.
  • Simplify installation and hardening by automating dependency installation, firewall and discovery configuration, and service unit creation.

Description

  • Add scripts/setup_hl_l1222_android_print.sh, a comprehensive installer that installs packages, enables services (cups, avahi-daemon, firewalld, optional bluetooth), configures firewall rules, and updates CUPS sharing including a backup of /etc/cups/cupsd.conf and user confirmation prompts.
  • Implement automatic USB device URI detection and model selection (with preference for printer-driver-brlaser or fallback to everywhere/generic), then create or replace the CUPS queue using lpadmin and related commands.
  • Install and enable a hotfolder auto-print watcher using inotifywait plus a systemd unit at /etc/systemd/system/ to auto-print files dropped into /var/spool/hll1222-hotfolder.
  • Add an optional Python-based LAN upload service (upload_server.py) and systemd unit for simple browser uploads to the hotfolder, and add a README.md describing usage and the run command sudo bash scripts/setup_hl_l1222_android_print.sh.

Testing

  • No automated tests were run against these changes.

Codex Task


Open in Devin Review

Summary by Sourcery

Add a configurable setup script and documentation to turn a Raspberry Pi (openSUSE Tumbleweed) into a CUPS-based network print server for a USB-connected Brother HL-L1222/HL-L1222-V, including optional hotfolder and browser upload workflows.

New Features:

  • Provide a one-step Bash setup script to install dependencies, configure services, and expose a CUPS print queue for the Brother HL-L1222/HL-L1222-V over IPP.
  • Add an optional hotfolder auto-print service that watches a spool directory and prints supported files automatically.
  • Introduce an optional lightweight LAN upload web service for browser-based file uploads into the hotfolder.

Enhancements:

  • Document the Android-over-Wi-Fi IPP printing workflow and usage of the new setup script in the README, including key connection details and caveats about Bluetooth.

Summary by cubic

Add a one-step setup script and README to turn a Raspberry Pi (openSUSE Tumbleweed) into an IPP/Mopria print server for a USB Brother HL‑L1222/HL‑L1222‑V. This enables Android printing over the same Wi‑Fi, with optional hotfolder and simple browser upload.

  • New Features
    • Add scripts/setup_hl_l1222_android_print.sh to install dependencies and enable services (cups, avahi-daemon, firewalld, optional bluetooth).
    • Configure CUPS sharing with a backup of /etc/cups/cupsd.conf and confirmation prompts; auto-detect USB URI and create/replace queue Brother_HL_L1222_V with model fallback (printer-driver-brlasereverywhere → generic).
    • Advertise via Avahi for IPP/Mopria and open firewall for IPP/mDNS (and upload port when enabled).
    • Add hotfolder auto-print service (hll1222-hotfolder) using inotifywait to print files dropped into /var/spool/hll1222-hotfolder.
    • Optional lightweight upload service (upload_server.py, hll1222-upload) to accept browser uploads into the hotfolder.
    • Update README with quickstart and Android Wi‑Fi printing steps.

Written for commit dcd749d. Summary will update on new commits. Review in cubic

@sourcery-ai

sourcery-ai Bot commented Apr 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new Bash setup script and documentation to turn a Raspberry Pi (openSUSE Tumbleweed) into a CUPS/IPP print server for a Brother HL‑L1222(V), including automated dependency installation, CUPS/firewalld/Avahi configuration, printer queue creation, a hotfolder auto‑print systemd service, and an optional LAN upload web service.

Sequence diagram for setup script end-to-end configuration

sequenceDiagram
  actor User
  participant Script as SetupScript
  participant Zypper
  participant Systemd
  participant Firewalld
  participant CUPS
  participant Avahi as AvahiDaemon
  participant USB as USBSubsystem

  User->>Script: Run setup_hl_l1222_android_print.sh
  Script->>Zypper: Install cups, avahi, firewalld, drivers, tools
  Script->>Systemd: enable --now cups.service
  Script->>Systemd: enable --now avahi-daemon.service
  Script->>Systemd: enable --now firewalld.service
  Script->>Systemd: enable --now bluetooth.service (optional)

  Script->>Firewalld: Add ipp and mdns services
  Script->>Firewalld: Add upload_port (optional)
  Script->>Firewalld: Reload rules

  Script->>User: Prompt to modify cupsd.conf
  User-->>Script: Confirm
  Script->>CUPS: cupsctl --share-printers --remote-any
  Script->>CUPS: Append WebInterface, Browsing, BrowseLocalProtocols
  Script->>Systemd: Restart cups.service

  Script->>CUPS: lpinfo -v (detect USB)
  CUPS->>USB: Enumerate USB printers
  CUPS-->>Script: Device URI for Brother

  Script->>CUPS: lpinfo -m (select model)
  alt Existing queue
    Script->>User: Ask to delete existing queue
    User-->>Script: Confirm
    Script->>CUPS: lpadmin -x QUEUE
  end
  Script->>CUPS: lpadmin -p QUEUE -E -v URI -m MODEL
  Script->>CUPS: Set defaults, share, enable, accept

  Script->>Systemd: Install and enable hll1222-hotfolder.service
  Script->>Systemd: Install and enable hll1222-upload.service (optional)
  Script-->>User: Print connection and usage summary
Loading

Sequence diagram for LAN upload to hotfolder printing

sequenceDiagram
  actor User
  participant Browser
  participant Upload as UploadService_hll1222-upload
  participant Hotfolder as HotfolderDirectory
  participant Watcher as HotfolderWatcher_hll1222-hotfolder
  participant CUPS
  participant Printer

  User->>Browser: Open http://pi:UPLOAD_PORT/
  Browser->>Upload: GET /
  Upload-->>Browser: HTML upload form

  User->>Browser: Select file and submit
  Browser->>Upload: POST /upload (multipart file)
  Upload->>Upload: Validate size and extension
  Upload->>Hotfolder: Write file into hotfolder
  Upload-->>Browser: Success response

  Watcher->>Hotfolder: Monitor with inotifywait
  Hotfolder-->>Watcher: Event close_write or moved_to
  Watcher->>CUPS: lp -d QUEUE file
  CUPS->>Printer: Send job via USB
  Printer-->>User: Printed document
Loading

Class diagram for upload server HTTP handler and environment

classDiagram
  class H {
    +send_body(code int, body str, ctype str)
    +do_GET()
    +do_POST()
  }

  class ThreadingHTTPServer {
  }

  class EnvironmentConfig {
    +hot : Path
    +queue : str
    +maxb : int
    +allowed : set
  }

  EnvironmentConfig <.. H : uses
  ThreadingHTTPServer o-- H : handler
Loading

Flow diagram for hotfolder auto-print systemd service

flowchart LR
  subgraph System["Raspberry Pi"]
    HF["Hotfolder /var/spool/hll1222-hotfolder"]
    W["watch_print.sh (inotifywait loop)"]
    SVC["Systemd service hll1222-hotfolder"]
    CUPS["CUPS queue Brother_HL_L1222_V"]
  end
  P["USB Brother HL-L1222(V)"]

  SVC --> W
  W --> HF
  HF -->|New file event| W
  W -->|lp -d QUEUE file| CUPS
  CUPS -->|Print job| P
Loading

File-Level Changes

Change Details Files
Introduce a one-step Bash installer that provisions the Pi as a CUPS-based IPP print server for a Brother HL-L1222(V), including dependency installation, service enablement, firewall rules, and CUPS sharing configuration.
  • Add argument parsing to control queue name, device/model overrides, Bluetooth and upload toggles, status-only mode, and upload port.
  • Ensure required tools (sudo, zypper, lpinfo, lpadmin, lpstat) are installed and resolvable in PATH, with helper to locate binaries in sbin paths.
  • Install base printing/network packages (CUPS, filters, ghostscript, avahi, firewalld, usbutils, Python multipart) and optionally brlaser and Bluetooth packages via zypper.
  • Enable and start cups, avahi-daemon, firewalld, and optionally bluetooth via systemd.
  • Configure firewalld to allow IPP and mDNS traffic, and optionally the upload HTTP port, then reload rules.
  • Backup cupsd.conf with a timestamped copy, then use cupsctl plus direct edits to enable printer sharing, remote access, WebInterface, and dnssd browsing, followed by CUPS restart.
  • Detect a Brother USB device URI from lpinfo output when not provided and choose an appropriate model (prefer brlaser/Brother, fallback to everywhere or generic mono laser).
  • Create or replace the target CUPS queue with lpadmin, setting default options, enabling/sharing it, accepting jobs, and making it the default printer, then restart CUPS.
  • Provide a status-report mode that prints system, USB, CUPS devices/models, and queue status without making changes.
  • Print a final summary with discovered IP, queue URI for Android IPP, and optional upload/Bluetooth notes.
scripts/setup_hl_l1222_android_print.sh
Add a hotfolder-based auto-print pipeline with a systemd unit that watches a spool directory and prints supported files automatically to the configured queue.
  • Create a dedicated script directory under /usr/local/lib and a writable hotfolder under /var/spool for the HL-L1222 workflow.
  • Install or ensure inotify-tools is available, then write a watch_print.sh script that inotify-watches the hotfolder for new/closed files.
  • Implement watcher logic to auto-submit supported file types (PDF/images/text) to the configured queue with lp, then delete them.
  • Define and install a systemd service unit that runs the watcher with appropriate environment variables, depends on CUPS, restarts on failure, and is enabled and started at boot.
scripts/setup_hl_l1222_android_print.sh
Provide an optional LAN upload web service backed by Python and systemd that drops uploaded files into the hotfolder for printing.
  • Gate upload service installation on a flag and on availability of the python3-python-multipart module, falling back gracefully when missing.
  • Generate and install a Python HTTP server script that exposes a simple HTML upload form, validates multipart uploads, enforces size and extension limits, and writes files into the hotfolder with safe filenames and permissions.
  • Create a dedicated systemd unit running the upload server as lp:lp, wiring in environment variables for queue, hotfolder, port, and max size, with network/CUPS dependencies and restart policy, then enable and start it.
  • Open the upload port in firewalld when the upload service is enabled.
scripts/setup_hl_l1222_android_print.sh
Document the new setup workflow and Android IPP usage in the repository README.
  • Replace the placeholder README content with a description of the Raspberry Pi HL-L1222-V Android print server setup.
  • Document the setup script path and the canonical run command using sudo bash.
  • Explain Wi-Fi IPP as the recommended Android printing path, note the limitations of Bluetooth, and describe how to configure Android Mopria with the resulting IPP URI.
  • Mention that the script defaults to the CUPS everywhere model and that users with an official Brother PPD can adjust the queue model afterward.
README.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The --status flag likely never works as intended because STATUS_ONLY is declared local in main() but set in parse_args(), so parse_args only updates the global variable while the if [[ "${STATUS_ONLY:-no}" == "yes" ]] check reads the untouched local; consider using either a single global or passing the flag explicitly instead of mixing scopes.
  • In print_summary, the line echo " $LPSTAT_BIN -t" is fine but the next one echo " "$LPINFO_BIN" -m | grep ..." has mismatched quoting and will be split by the shell; wrap the whole command string in one pair of quotes or use printf to avoid accidental word-splitting.
  • The hotfolder is created world-writable (chmod 0777), which is convenient but quite permissive on a multi-user system; consider tightening permissions (e.g., a dedicated group plus setgid bit) so only trusted users and the upload service can write into it.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `--status` flag likely never works as intended because `STATUS_ONLY` is declared `local` in `main()` but set in `parse_args()`, so `parse_args` only updates the global variable while the `if [[ "${STATUS_ONLY:-no}" == "yes" ]]` check reads the untouched local; consider using either a single global or passing the flag explicitly instead of mixing scopes.
- In `print_summary`, the line `echo "  $LPSTAT_BIN -t"` is fine but the next one `echo "  "$LPINFO_BIN" -m | grep ..."` has mismatched quoting and will be split by the shell; wrap the whole command string in one pair of quotes or use printf to avoid accidental word-splitting.
- The hotfolder is created world-writable (`chmod 0777`), which is convenient but quite permissive on a multi-user system; consider tightening permissions (e.g., a dedicated group plus setgid bit) so only trusted users and the upload service can write into it.

## Individual Comments

### Comment 1
<location path="scripts/setup_hl_l1222_android_print.sh" line_range="501-510" />
<code_context>
+hot = Path(os.environ.get("PRINT_HOTFOLDER", "/var/spool/hll1222-hotfolder"))
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `item.raw` from `python-multipart` is likely incorrect and may break uploads.

`parse_form_data` usually returns a file-like object where content is read via `item.file.read()`, not a `.raw` attribute, so this will likely fail at runtime.

Use an explicit read from the file object and apply the same size limit to the bytes you actually write:
```python
data = item.file.read(maxb + 1)
if len(data) > maxb:
    self.send_body(413, "Invalid upload size")
    return
with open(dst, "wb") as f:
    f.write(data)
```
This avoids relying on a non-standard attribute and keeps the size check consistent with the stored content.
</issue_to_address>

### Comment 2
<location path="scripts/setup_hl_l1222_android_print.sh" line_range="391-399" />
<code_context>
+    err "Failed to create script directory ${SCRIPT_DIR}."
+    exit 1
+  fi
+  if ! sudo install -d -m 0775 -o lp -g lp "$HOTFOLDER"; then
+    err "Failed to create hotfolder ${HOTFOLDER}."
+    exit 1
+  fi
+  # Keep folder writable for upload service and watcher workflows.
+  if ! sudo chmod 0777 "$HOTFOLDER"; then
+    err "Failed to set writable permissions on ${HOTFOLDER}."
+    exit 1
</code_context>
<issue_to_address>
**🚨 suggestion (security):** World-writable hotfolder (`0777`) is risky; consider tightening permissions.

The directory is first created with `lp:lp` ownership and then broadened to `0777`, which exposes it to writes from any local user or compromised service. Given that the upload service runs as `lp:lp` and the watcher as `root`, it should be sufficient to keep it group-owned by `lp` and writable only to that user/group, for example:
```bash
sudo install -d -m 0770 -o lp -g lp "$HOTFOLDER"
```
If multiple writers are required, use a dedicated group instead of making the directory globally writable.

```suggestion
  # Create hotfolder owned by lp:lp; writable by owner and group only.
  # The upload service runs as lp:lp and the watcher runs as root, so 0770 is sufficient.
  if ! sudo install -d -m 0770 -o lp -g lp "$HOTFOLDER"; then
    err "Failed to create hotfolder ${HOTFOLDER}."
    exit 1
  fi
```
</issue_to_address>

### Comment 3
<location path="scripts/setup_hl_l1222_android_print.sh" line_range="529-536" />
<code_context>
+        if "multipart/form-data" not in ctype:
+            self.send_body(400, "Content-Type must be multipart/form-data")
+            return
+        clen = int(self.headers.get("Content-Length", "0"))
+        if clen <= 0 or clen > maxb:
+            self.send_body(413, "Invalid upload size")
+            return
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Content-Length parsing can raise and bypass your validation logic.

`int(self.headers.get("Content-Length", "0"))` will raise `ValueError` if the header is missing or non-numeric, causing a 500 instead of your intended 4xx.

Consider guarding the conversion:
```python
try:
    clen = int(self.headers.get("Content-Length", "0"))
except ValueError:
    self.send_body(411, "Invalid or missing Content-Length")
    return
if clen <= 0 or clen > maxb:
    self.send_body(413, "Invalid upload size")
    return
```
so malformed headers are handled cleanly instead of crashing the handler.

```suggestion
        ctype = self.headers.get("Content-Type", "")
        if "multipart/form-data" not in ctype:
            self.send_body(400, "Content-Type must be multipart/form-data")
            return
        try:
            clen = int(self.headers.get("Content-Length", "0"))
        except ValueError:
            self.send_body(411, "Invalid or missing Content-Length")
            return
        if clen <= 0 or clen > maxb:
            self.send_body(413, "Invalid upload size")
            return
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +501 to +510
hot = Path(os.environ.get("PRINT_HOTFOLDER", "/var/spool/hll1222-hotfolder"))
queue = os.environ.get("PRINT_QUEUE", "Brother_HL_L1222_V")
maxb = int(os.environ.get("MAX_UPLOAD_BYTES", "52428800"))
allowed = {".pdf", ".txt", ".png", ".jpg", ".jpeg", ".tif", ".tiff"}

hot.mkdir(parents=True, exist_ok=True)

page = f"""<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>HL-L1222 Upload</title></head><body><h1>HL-L1222 Upload</h1><p>Queue: <b>{html.escape(queue)}</b></p><form method='post' enctype='multipart/form-data' action='/upload'><input type='file' name='file' required><button type='submit'>Upload</button></form><p>LAN only. Allowed: PDF/TXT/PNG/JPG/TIFF</p></body></html>"""

class H(BaseHTTPRequestHandler):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Using item.raw from python-multipart is likely incorrect and may break uploads.

parse_form_data usually returns a file-like object where content is read via item.file.read(), not a .raw attribute, so this will likely fail at runtime.

Use an explicit read from the file object and apply the same size limit to the bytes you actually write:

data = item.file.read(maxb + 1)
if len(data) > maxb:
    self.send_body(413, "Invalid upload size")
    return
with open(dst, "wb") as f:
    f.write(data)

This avoids relying on a non-standard attribute and keeps the size check consistent with the stored content.

Comment on lines +391 to +399
if ! sudo install -d -m 0775 -o lp -g lp "$HOTFOLDER"; then
err "Failed to create hotfolder ${HOTFOLDER}."
exit 1
fi
# Keep folder writable for upload service and watcher workflows.
if ! sudo chmod 0777 "$HOTFOLDER"; then
err "Failed to set writable permissions on ${HOTFOLDER}."
exit 1
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 suggestion (security): World-writable hotfolder (0777) is risky; consider tightening permissions.

The directory is first created with lp:lp ownership and then broadened to 0777, which exposes it to writes from any local user or compromised service. Given that the upload service runs as lp:lp and the watcher as root, it should be sufficient to keep it group-owned by lp and writable only to that user/group, for example:

sudo install -d -m 0770 -o lp -g lp "$HOTFOLDER"

If multiple writers are required, use a dedicated group instead of making the directory globally writable.

Suggested change
if ! sudo install -d -m 0775 -o lp -g lp "$HOTFOLDER"; then
err "Failed to create hotfolder ${HOTFOLDER}."
exit 1
fi
# Keep folder writable for upload service and watcher workflows.
if ! sudo chmod 0777 "$HOTFOLDER"; then
err "Failed to set writable permissions on ${HOTFOLDER}."
exit 1
fi
# Create hotfolder owned by lp:lp; writable by owner and group only.
# The upload service runs as lp:lp and the watcher runs as root, so 0770 is sufficient.
if ! sudo install -d -m 0770 -o lp -g lp "$HOTFOLDER"; then
err "Failed to create hotfolder ${HOTFOLDER}."
exit 1
fi

Comment on lines +529 to +536
ctype = self.headers.get("Content-Type", "")
if "multipart/form-data" not in ctype:
self.send_body(400, "Content-Type must be multipart/form-data")
return
clen = int(self.headers.get("Content-Length", "0"))
if clen <= 0 or clen > maxb:
self.send_body(413, "Invalid upload size")
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Content-Length parsing can raise and bypass your validation logic.

int(self.headers.get("Content-Length", "0")) will raise ValueError if the header is missing or non-numeric, causing a 500 instead of your intended 4xx.

Consider guarding the conversion:

try:
    clen = int(self.headers.get("Content-Length", "0"))
except ValueError:
    self.send_body(411, "Invalid or missing Content-Length")
    return
if clen <= 0 or clen > maxb:
    self.send_body(413, "Invalid upload size")
    return

so malformed headers are handled cleanly instead of crashing the handler.

Suggested change
ctype = self.headers.get("Content-Type", "")
if "multipart/form-data" not in ctype:
self.send_body(400, "Content-Type must be multipart/form-data")
return
clen = int(self.headers.get("Content-Length", "0"))
if clen <= 0 or clen > maxb:
self.send_body(413, "Invalid upload size")
return
ctype = self.headers.get("Content-Type", "")
if "multipart/form-data" not in ctype:
self.send_body(400, "Content-Type must be multipart/form-data")
return
try:
clen = int(self.headers.get("Content-Length", "0"))
except ValueError:
self.send_body(411, "Invalid or missing Content-Length")
return
if clen <= 0 or clen > maxb:
self.send_body(413, "Invalid upload size")
return

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dcd749d112

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +191 to +194
if ! sudo zypper --non-interactive install --no-recommends \
cups cups-client cups-filters ghostscript \
avahi nss-mdns firewalld usbutils python3 python3-python-multipart; then
err "Failed to install required base packages."

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make python-multipart install conditional on upload option

The base dependency install always includes python3-python-multipart, even though the upload server is optional and disabled by default. On repos where that package is missing (which the script already anticipates later), install_dependencies exits early and the whole printer setup fails before reaching the normal IPP workflow. This should be gated behind ENABLE_UPLOAD (or treated as optional) so default installs are not blocked by an optional feature.

Useful? React with 👍 / 👎.

Comment on lines +421 to +422
lp -d "$QUEUE" -o fit-to-page "$file" || true
rm -f "$file" || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve files when hotfolder print submission fails

The watcher deletes each file unconditionally right after lp, even when print submission fails (lp ... || true). If CUPS is down, the queue is paused, or lp rejects the job, documents are silently removed and unrecoverable, which is data loss for users relying on the hotfolder. Deletion should only occur after successful submission.

Useful? React with 👍 / 👎.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 7 potential issues.

Open in Devin Review

Comment thread README.md

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚩 README instructs sudo but script documents running as normal user

The README at line 34 instructs sudo bash scripts/setup_hl_l1222_android_print.sh, while the script's own header (line 15-16) says to run as a normal user with sudo used internally. Running the entire script as root means sudo calls are redundant (harmless) but lpoptions -d at line 373 would set the default printer for root only, not for the normal user who will use the printer. This inconsistency is worth aligning in documentation.

(Refers to line 34)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +556 to +557
safe = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name)[:160]
dst = hot / safe

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 Filename truncation can strip extension, causing uploaded files to silently never be printed

At line 556 the sanitized filename is truncated to 160 characters: safe = "...")[:160]. If the original filename exceeds 160 chars, the truncation can remove the file extension (e.g., .pdf). The file is then saved to the hotfolder at line 557 (dst = hot / safe) without its extension. The hotfolder watcher script (scripts/setup_hl_l1222_android_print.sh:419-420) only processes files matching *.pdf|*.jpg|*.jpeg|*.png|*.txt|*.tif|*.tiff, so the extensionless file is silently ignored — never printed and never cleaned up. Meanwhile, the user receives a "Uploaded successfully" response at line 565, creating a silent failure where the user believes the file was queued for printing but it never prints.

Note on the collision-avoidance path

The collision loop at lines 559-561 uses the original ext variable, so it would correctly append the extension — but only when a file with the same name already exists. On the first (non-colliding) upload, the truncated safe without extension is used directly.

Suggested change
safe = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name)[:160]
dst = hot / safe
stem = ".".join(name.split(".")[:-1]) if "." in name else name
safe_stem = ".".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in stem)
safe = (safe_stem[:160 - len(ext)] + ext) if len(safe_stem) + len(ext) > 160 else safe_stem + ext
safe = ".".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name) if len(safe) <= 160 else safe
dst = hot / safe
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

exit 1
fi

TMP_DIR=""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: TMP_DIR is leaked on normal execution path

Both install_hotfolder_service (line 401) and install_upload_service_optional (line 488) create temp dirs via mktemp -d, but at lines 472 and 623 they set TMP_DIR="" rather than removing the temp dir. The cleanup trap (line 62-66) only runs on EXIT/SIGINT/SIGTERM, and by then TMP_DIR is empty, so the temp dirs are never removed. For a one-shot setup script this is a very minor resource leak, but for correctness the functions should rm -rf the temp dir before clearing the variable, or simply not clear it so the EXIT trap handles it.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +114 to +145
--queue)
QUEUE_NAME="$2"
shift 2
;;
--device)
DEVICE_URI="$2"
shift 2
;;
--model)
MODEL_NAME="$2"
shift 2
;;
--enable-bluetooth)
ENABLE_BLUETOOTH="yes"
shift
;;
--disable-bluetooth)
ENABLE_BLUETOOTH="no"
shift
;;
--enable-upload)
ENABLE_UPLOAD="yes"
shift
;;
--disable-upload)
ENABLE_UPLOAD="no"
shift
;;
--upload-port)
UPLOAD_PORT="$2"
shift 2
;;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: parse_args with set -u gives cryptic error on missing option arguments

Options like --queue, --device, --model, and --upload-port access $2 without checking that a second argument exists (e.g., lines 115, 119, 123, 143). With set -u (from set -euo pipefail at line 2), running ./script.sh --queue without a value triggers bash: $2: unbound variable — a valid but user-unfriendly error. This isn't a correctness bug since the script safely aborts, but adding a guard like [[ $# -ge 2 ]] || { err "--queue requires a value"; exit 1; } before accessing $2 would improve usability.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +657 to +658
local STATUS_ONLY="no"
parse_args "$@"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: STATUS_ONLY variable scoping works correctly via bash dynamic scoping

main() declares local STATUS_ONLY="no" at line 657, then calls parse_args at line 658 which may set STATUS_ONLY="yes" at line 147. This works correctly because bash uses dynamic scoping — assignments in called functions modify the caller's local variable. I verified this is not a scoping bug: the --status flag will be properly detected at line 675.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

if "multipart/form-data" not in ctype:
self.send_body(400, "Content-Type must be multipart/form-data")
return
clen = int(self.headers.get("Content-Length", "0"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Upload server Content-Length parsing could raise ValueError on malformed input

At line 533 in the embedded Python server, clen = int(self.headers.get("Content-Length", "0")) would raise ValueError if a malicious client sends a non-numeric Content-Length header (e.g., Content-Length: abc). Since ThreadingHTTPServer handles each request in a separate thread, only the individual handler thread would crash — the server continues running. This is a minor robustness issue for a LAN-only service, but wrapping in a try/except would be more robust.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

exit 1
fi
# Keep folder writable for upload service and watcher workflows.
if ! sudo chmod 0777 "$HOTFOLDER"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📝 Info: Hotfolder has world-writable permissions (0777)

At line 396, the hotfolder is set to chmod 0777. The comment explains this is intentional for the upload service (running as lp user) and watcher (running as root). However, this means any local user on the system can drop files into the hotfolder to be printed. For a single-purpose Raspberry Pi this is acceptable, but on a shared system it could be an abuse vector (e.g., anyone can trigger unlimited print jobs). Consider using group-based permissions instead (e.g., 0775 with appropriate group membership).

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a comprehensive setup script and documentation for configuring a Raspberry Pi as a print server for Brother HL-L1222 printers on openSUSE Tumbleweed. The review feedback identifies several critical improvements: correcting the README to avoid unnecessary root execution, fixing a bug that causes temporary directory leaks by resetting the TMP_DIR variable, enhancing security by running services with reduced privileges and tightening file permissions, and improving the robustness of script argument parsing and command efficiency.

Comment thread README.md

## Run
```bash
sudo bash scripts/setup_hl_l1222_android_print.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The command sudo bash ... runs the entire script as root. However, the script is designed to be run as a normal user, and it uses sudo internally for privileged operations (as documented in scripts/setup_hl_l1222_android_print.sh on lines 15-16). Running the whole script as root is unnecessary, goes against the script's design, and could lead to unexpected behavior.

Suggested change
sudo bash scripts/setup_hl_l1222_android_print.sh
bash scripts/setup_hl_l1222_android_print.sh

Comment on lines +62 to +67
cleanup() {
if [[ -n "${TMP_DIR}" && -d "${TMP_DIR}" ]]; then
rm -rf "${TMP_DIR}"
fi
}
trap cleanup EXIT SIGINT SIGTERM

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The cleanup function and trap are correctly set up to remove temporary directories on exit. However, this is defeated because the TMP_DIR variable is reset to an empty string within the functions that use it (install_hotfolder_service at line 472 and install_upload_service_optional at line 623). This causes temporary directories created by mktemp to be leaked.

To fix this, please remove the TMP_DIR="" statements from line 472 and line 623.

Comment on lines +445 to +446
User=root
Group=root

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Running the hotfolder watcher service as root poses a security risk, as it processes files from a directory that could be writable by other users. It's best practice to run services with the least privilege necessary. The lp user should have sufficient permissions for this task.

Suggested change
User=root
Group=root
User=lp
Group=lp

Comment on lines +115 to +116
QUEUE_NAME="$2"
shift 2

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The argument parsing logic is not robust against missing option arguments. For example, if the script is called with --queue but no value, $2 will be the next option (e.g., --device), which is then incorrectly assigned to QUEUE_NAME. You should add a check to ensure an argument is provided and it's not another option. This should be applied to all options that require an argument.

Suggested change
QUEUE_NAME="$2"
shift 2
if [[ -z "$2" || "$2" =~ ^- ]]; then
err "Option --queue requires an argument."
usage
exit 1
fi
QUEUE_NAME="$2"
shift 2

fi

# 2) Fallback to everywhere if available.
if "$LPINFO_BIN" -m | awk '{print $1}' | grep -qx 'everywhere'; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The pipeline of awk and grep to check for the 'everywhere' model is inefficient. It can be simplified into a single, more performant awk command that exits as soon as a match is found.

Suggested change
if "$LPINFO_BIN" -m | awk '{print $1}' | grep -qx 'everywhere'; then
if "$LPINFO_BIN" -m | awk '$1 == "everywhere" { exit 0 } END { exit 1 }'; then

exit 1
fi
# Keep folder writable for upload service and watcher workflows.
if ! sudo chmod 0777 "$HOTFOLDER"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using chmod 0777 for the hotfolder is excessively permissive and a security risk, as it allows any user on the system to modify its contents. Given that the services using this folder run as lp or root, 0775 permissions would be more appropriate and secure.

Suggested change
if ! sudo chmod 0777 "$HOTFOLDER"; then
if ! sudo chmod 0775 "$HOTFOLDER"; then

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

7 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:29">
P3: The README incorrectly documents `everywhere` as the default model; the script actually prefers Brother/brlaser and only falls back to `everywhere`.</violation>
</file>

<file name="scripts/setup_hl_l1222_android_print.sh">

<violation number="1" location="scripts/setup_hl_l1222_android_print.sh:193">
P1: Don't make the optional upload dependency a hard requirement. Installing `python3-python-multipart` in the base package set can abort the entire setup even when upload support is disabled.</violation>

<violation number="2" location="scripts/setup_hl_l1222_android_print.sh:396">
P1: The hotfolder is set to `0777`, allowing any local user to inject or tamper with print jobs. Restrict it to the service account/group.</violation>

<violation number="3" location="scripts/setup_hl_l1222_android_print.sh:421">
P1: Only delete the hotfolder file after `lp` succeeds. The current flow removes files even when print submission fails, which can silently drop user documents.</violation>

<violation number="4" location="scripts/setup_hl_l1222_android_print.sh:445">
P1: The hotfolder watcher runs as root while processing user-supplied files. Run it as a non-root account (e.g., `lp`) to reduce privilege exposure.</violation>

<violation number="5" location="scripts/setup_hl_l1222_android_print.sh:533">
P2: Guard `Content-Length` parsing with `try/except` so malformed headers return a controlled 4xx response instead of raising in the request handler.</violation>

<violation number="6" location="scripts/setup_hl_l1222_android_print.sh:556">
P1: Preserve the original extension when truncating long filenames. Truncating the full name to 160 characters can strip `.pdf/.jpg/...`, causing the watcher to skip the uploaded file.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

exit 1
fi
# Keep folder writable for upload service and watcher workflows.
if ! sudo chmod 0777 "$HOTFOLDER"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The hotfolder is set to 0777, allowing any local user to inject or tamper with print jobs. Restrict it to the service account/group.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 396:

<comment>The hotfolder is set to `0777`, allowing any local user to inject or tamper with print jobs. Restrict it to the service account/group.</comment>

<file context>
@@ -0,0 +1,690 @@
+    exit 1
+  fi
+  # Keep folder writable for upload service and watcher workflows.
+  if ! sudo chmod 0777 "$HOTFOLDER"; then
+    err "Failed to set writable permissions on ${HOTFOLDER}."
+    exit 1
</file context>


[Service]
Type=simple
User=root

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The hotfolder watcher runs as root while processing user-supplied files. Run it as a non-root account (e.g., lp) to reduce privilege exposure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 445:

<comment>The hotfolder watcher runs as root while processing user-supplied files. Run it as a non-root account (e.g., `lp`) to reduce privilege exposure.</comment>

<file context>
@@ -0,0 +1,690 @@
+
+[Service]
+Type=simple
+User=root
+Group=root
+Environment=PRINT_QUEUE=${QUEUE_NAME}
</file context>

if ext not in allowed:
self.send_body(400, f"Unsupported file type: {html.escape(ext)}")
return
safe = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name)[:160]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Preserve the original extension when truncating long filenames. Truncating the full name to 160 characters can strip .pdf/.jpg/..., causing the watcher to skip the uploaded file.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 556:

<comment>Preserve the original extension when truncating long filenames. Truncating the full name to 160 characters can strip `.pdf/.jpg/...`, causing the watcher to skip the uploaded file.</comment>

<file context>
@@ -0,0 +1,690 @@
+        if ext not in allowed:
+            self.send_body(400, f"Unsupported file type: {html.escape(ext)}")
+            return
+        safe = "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in name)[:160]
+        dst = hot / safe
+        i = 1
</file context>

inotifywait -m "$HOTFOLDER" -e close_write -e moved_to --format '%w%f' | while read -r file; do
case "${file,,}" in
*.pdf|*.jpg|*.jpeg|*.png|*.txt|*.tif|*.tiff)
lp -d "$QUEUE" -o fit-to-page "$file" || true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Only delete the hotfolder file after lp succeeds. The current flow removes files even when print submission fails, which can silently drop user documents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 421:

<comment>Only delete the hotfolder file after `lp` succeeds. The current flow removes files even when print submission fails, which can silently drop user documents.</comment>

<file context>
@@ -0,0 +1,690 @@
+inotifywait -m "$HOTFOLDER" -e close_write -e moved_to --format '%w%f' | while read -r file; do
+  case "${file,,}" in
+    *.pdf|*.jpg|*.jpeg|*.png|*.txt|*.tif|*.tiff)
+      lp -d "$QUEUE" -o fit-to-page "$file" || true
+      rm -f "$file" || true
+      ;;
</file context>

log "Installing required packages..."
if ! sudo zypper --non-interactive install --no-recommends \
cups cups-client cups-filters ghostscript \
avahi nss-mdns firewalld usbutils python3 python3-python-multipart; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: Don't make the optional upload dependency a hard requirement. Installing python3-python-multipart in the base package set can abort the entire setup even when upload support is disabled.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 193:

<comment>Don't make the optional upload dependency a hard requirement. Installing `python3-python-multipart` in the base package set can abort the entire setup even when upload support is disabled.</comment>

<file context>
@@ -0,0 +1,690 @@
+  log "Installing required packages..."
+  if ! sudo zypper --non-interactive install --no-recommends \
+    cups cups-client cups-filters ghostscript \
+    avahi nss-mdns firewalld usbutils python3 python3-python-multipart; then
+    err "Failed to install required base packages."
+    exit 1
</file context>
Suggested change
avahi nss-mdns firewalld usbutils python3 python3-python-multipart; then
avahi nss-mdns firewalld usbutils python3; then

if "multipart/form-data" not in ctype:
self.send_body(400, "Content-Type must be multipart/form-data")
return
clen = int(self.headers.get("Content-Length", "0"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Guard Content-Length parsing with try/except so malformed headers return a controlled 4xx response instead of raising in the request handler.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/setup_hl_l1222_android_print.sh, line 533:

<comment>Guard `Content-Length` parsing with `try/except` so malformed headers return a controlled 4xx response instead of raising in the request handler.</comment>

<file context>
@@ -0,0 +1,690 @@
+        if "multipart/form-data" not in ctype:
+            self.send_body(400, "Content-Type must be multipart/form-data")
+            return
+        clen = int(self.headers.get("Content-Length", "0"))
+        if clen <= 0 or clen > maxb:
+            self.send_body(413, "Invalid upload size")
</file context>

Comment thread README.md
- `ipp://<raspberrypi-ip>/printers/Brother_HL_L1222_V`

## Notes
- Script uses CUPS `everywhere` model as default for broad compatibility.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The README incorrectly documents everywhere as the default model; the script actually prefers Brother/brlaser and only falls back to everywhere.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At README.md, line 29:

<comment>The README incorrectly documents `everywhere` as the default model; the script actually prefers Brother/brlaser and only falls back to `everywhere`.</comment>

<file context>
@@ -1,3 +1,30 @@
+   - `ipp://<raspberrypi-ip>/printers/Brother_HL_L1222_V`
+
+## Notes
+- Script uses CUPS `everywhere` model as default for broad compatibility.
+- If you have an official Brother HL-L1222-V Linux PPD/driver package, install it and adjust queue model if desired.
</file context>
Suggested change
- Script uses CUPS `everywhere` model as default for broad compatibility.
- Script prefers a Brother/brlaser model when available, then falls back to CUPS `everywhere` (and generic models) for compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants