Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .docker/kasm/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
# Go toolchain under QEMU.
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS sidecar
ARG TARGETOS TARGETARCH
ARG VERSION=dev
WORKDIR /src
COPY go.mod ./
COPY internal ./internal
COPY cmd/sidecar ./cmd/sidecar
ENV CGO_ENABLED=0
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/sidecar ./cmd/sidecar
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w -X github.com/dockette/viewdoc/internal/version.Version=$VERSION" -o /out/sidecar ./cmd/sidecar

# Base pinned by digest for reproducible builds; Dependabot bumps it
# (.github/dependabot.yml).
FROM kasmweb/ubuntu-noble-desktop:1.19.0@sha256:14594dcf30c33768485a7dc6cb7bf3b7bef25121743d30371cbc56b525d96c71
USER root
ARG VERSION=dev
LABEL org.opencontainers.image.version=$VERSION
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# wireshark-common preseed: we only ever read pcap files, never capture, so
Expand Down
63 changes: 46 additions & 17 deletions .docker/viewdoc.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ QUERY_ENV="/tmp/viewdoc.env"
LAST="/tmp/.viewdoc_last_url"
STATE="/tmp/.viewdoc_last_state"
LOG="/tmp/viewdoc.log"
# Latest outcome for the sidecar's GET /status (key=value lines).
STATUS="/tmp/viewdoc.status"
# A launcher that exits 0 faster than this handed the URL to an already
# running instance (chromium, soffice, xdg-open) rather than failing.
HANDOFF_SECS=5
Expand All @@ -25,6 +27,20 @@ log() {
printf '%s\n' "$msg" >&2 || true
}

# status <phase> [app] [error] — records the outcome of this request for the
# sidecar's GET /status. Never load-bearing, like log().
status() {
if {
printf 'phase=%s\n' "$1"
printf 'url=%s\n' "$(printf '%s' "$url" | tr '\n' ' ')"
printf 'app=%s\n' "${2:-}"
printf 'error=%s\n' "${3:-}"
printf 'updated=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
} > "$STATUS.$$" 2>/dev/null; then
mv -f "$STATUS.$$" "$STATUS" 2>/dev/null || true
fi
}

[ -r "$QUERY_ENV" ] || { log "no $QUERY_ENV; nothing to do"; exit 0; }
# shellcheck disable=SC1090
. "$QUERY_ENV"
Expand All @@ -35,13 +51,14 @@ if [ -z "$url" ]; then
exit 0
fi

# $STATE is "<pid> <running|handoff|exited>" for the launch that wrote $LAST.
# $STATE is "<pid> <running|handoff|exited> [rc]" for the launch that wrote
# $LAST (rc once it has exited).
# The URL counts as still open while that launch's process is alive, or when
# it handed off to an existing instance (whose window we can't track). A
# crashed or closed app ("exited") lets the same URL open again.
is_open() {
local pid state
read -r pid state < "$STATE" 2>/dev/null || return 1
read -r pid state _ < "$STATE" 2>/dev/null || return 1
case "$state" in
handoff) return 0 ;;
running) kill -0 "$pid" 2>/dev/null ;;
Expand All @@ -53,11 +70,12 @@ is_open() {
# "--version.mp4" would be parsed as an option. Only real URLs get through.
case "$url" in
http://*|https://*) ;;
*) log "not an http(s) URL, refusing: $url"; exit 0 ;;
*) log "not an http(s) URL, refusing: $url"; status refused "" "not an http(s) URL"; exit 0 ;;
esac

if [ -r "$LAST" ] && [ "$(cat "$LAST")" = "$url" ] && is_open; then
log "dedup hit, $url already open"
status deduped
exit 0
fi

Expand All @@ -68,10 +86,12 @@ download() {
local f
f="$(mktemp --suffix=".$ext" /tmp/viewdoc-dl.XXXXXXXX)"
log "download $url -> $f"
status downloading "$app"
if curl -fsSL --max-time 60 -o "$f" "$url"; then
printf '%s' "$f"
else
log "download failed url=$url"
status failed "$app" "download failed"
rm -f "$f"
return 1
fi
Expand All @@ -81,6 +101,13 @@ lower="$(printf '%s' "$url" | tr '[:upper:]' '[:lower:]')"
path="${lower%%\?*}"; path="${path%%#*}"
ext="${path##*.}"

# dispatch <app> names the app this request goes to (for logs + status).
app=""
dispatch() {
app="$1"
log "dispatch=$app ext=$ext url=$url"
}

run() {
log "exec: $*"
# tee fans the launched app's combined stdout/stderr to both the single
Expand All @@ -100,25 +127,26 @@ run() {
owner=""
read -r owner _ < "$STATE" 2>/dev/null || true
if [ "$owner" = "$BASHPID" ]; then
printf '%s %s\n' "$BASHPID" "$state" > "$STATE"
printf '%s %s %s\n' "$BASHPID" "$state" "$rc" > "$STATE"
fi
log "exit: $1 rc=$rc state=$state"
) &
# Recorded only once something actually launched, so a failed download
# doesn't make the next identical request a no-op.
printf '%s %s\n' "$!" running > "$STATE"
printf '%s' "$url" > "$LAST"
status launched "${app:-$1}"
}

case "$ext" in
mp4|mkv|webm|mov|avi|mpeg|mpg|m4v|3gp|3g2|ts|mts|m2ts|vob|wmv|asf|divx|ogv|mp3|wav|flac|ogg|m4a|aac|opus|mka|wma|aiff|ape|mid)
log "dispatch=vlc ext=$ext url=$url"
dispatch vlc
# --no-qt-privacy-ask suppresses VLC's first-run "Privacy and Network
# Access Policy" modal, which otherwise blocks playback until clicked.
run vlc --no-qt-privacy-ask --no-video-title-show "$url"
;;
doc|docx|odt|rtf|xls|xlsx|ods|csv|ppt|pptx|odp|odg|vsd|vsdx|pub|pages|key|numbers)
log "dispatch=soffice ext=$ext url=$url"
dispatch soffice
# soffice handles http(s) URLs natively via UCB. --view opens read-only,
# --norestore kills the "recover document?" prompt after a previous
# session, --nologo suppresses the splash. Visio / Publisher / Apple
Expand All @@ -130,13 +158,13 @@ case "$ext" in
# open. curl runs synchronously here because the sidecar already forked
# viewdoc.sh as a separate process, so blocking here doesn't delay the
# sidecar's HTTP response. RAR5 needs unrar (p7zip-full only reads RAR4).
log "dispatch=xarchiver ext=$ext url=$url"
dispatch xarchiver
if tmpfile="$(download)"; then
run xarchiver "$tmpfile"
fi
;;
pcap|pcapng|cap)
log "dispatch=wireshark ext=$ext url=$url"
dispatch wireshark
if tmpfile="$(download)"; then
run wireshark -r "$tmpfile"
fi
Expand All @@ -151,14 +179,15 @@ case "$ext" in
# rewritten every time because claws-mail saves its own copies on exit.
cfgdir="/tmp/viewdoc-claws"
inbox="$cfgdir/Mail/inbox"
log "dispatch=claws-mail ext=$ext url=$url"
dispatch claws-mail
if tmpfile="$(download)"; then
# Outlook .msg is an OLE compound file claws-mail can't read; msgconvert
# (libemail-outlook-message-perl) rewrites it as MIME first.
if [ "$ext" = msg ]; then
eml="${tmpfile%.msg}.eml"
if ! msgconvert --outfile "$eml" "$tmpfile" >&2; then
log "msgconvert failed url=$url"
status failed "$app" "msgconvert failed"
exit 0
fi
tmpfile="$eml"
Expand Down Expand Up @@ -192,21 +221,21 @@ CFG
tif|tiff|heic|heif|avif|ico|tga)
# Formats Chromium won't render. ristretto decodes via gdk-pixbuf, with
# heif-gdk-pixbuf / libavif-gdk-pixbuf adding HEIC and AVIF.
log "dispatch=ristretto ext=$ext url=$url"
dispatch ristretto
if tmpfile="$(download)"; then
run ristretto "$tmpfile"
fi
;;
epub|djvu|xps|oxps|cbz|cbr|cb7)
log "dispatch=atril ext=$ext url=$url"
dispatch atril
if tmpfile="$(download)"; then
run atril "$tmpfile"
fi
;;
txt|log|json|yaml|yml|xml|md|ini|conf|cfg|toml)
# Downloaded rather than handed to Chromium: servers often send these as
# application/octet-stream, which Chromium saves instead of showing.
log "dispatch=mousepad ext=$ext url=$url"
dispatch mousepad
if tmpfile="$(download)"; then
run mousepad "$tmpfile"
fi
Expand All @@ -218,19 +247,19 @@ CFG
fi
;;
dxf)
log "dispatch=librecad ext=$ext url=$url"
dispatch librecad
if tmpfile="$(download)"; then
run librecad "$tmpfile"
fi
;;
ttf|otf|ttc)
log "dispatch=gnome-font-viewer ext=$ext url=$url"
dispatch gnome-font-viewer
if tmpfile="$(download)"; then
run gnome-font-viewer "$tmpfile"
fi
;;
bin|exe|dll|so|o|elf|dat|dmp)
log "dispatch=ghex ext=$ext url=$url"
dispatch ghex
if tmpfile="$(download)"; then
run ghex "$tmpfile"
fi
Expand All @@ -244,12 +273,12 @@ CFG
# the container leaves a stale lock that wedges every new launch).
for b in chromium chromium-browser google-chrome chrome; do
if command -v "$b" >/dev/null 2>&1; then
log "dispatch=$b ext=$ext url=$url"
dispatch "$b"
run "$b" --no-sandbox --user-data-dir=/tmp/viewdoc-chromium --new-window "$url"
exit 0
fi
done
log "dispatch=xdg-open ext=$ext url=$url"
dispatch xdg-open
run xdg-open "$url"
;;
esac
5 changes: 4 additions & 1 deletion .docker/webtop/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
# Go toolchain under QEMU.
FROM --platform=$BUILDPLATFORM golang:1.27-alpine AS sidecar
ARG TARGETOS TARGETARCH
ARG VERSION=dev
WORKDIR /src
COPY go.mod ./
COPY internal ./internal
COPY cmd/sidecar ./cmd/sidecar
ENV CGO_ENABLED=0
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w" -o /out/sidecar ./cmd/sidecar
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -ldflags="-s -w -X github.com/dockette/viewdoc/internal/version.Version=$VERSION" -o /out/sidecar ./cmd/sidecar

# Base pinned by digest for reproducible builds; Dependabot bumps it
# (.github/dependabot.yml).
FROM lscr.io/linuxserver/webtop:ubuntu-xfce@sha256:c766271bf5de1c7c8c6cd7be1ae4015953f1b45a9325dbc1c94f7a163ff7ffc7
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ARG VERSION=dev
LABEL org.opencontainers.image.version=$VERSION

# wireshark-common preseed: we only ever read pcap files, never capture, so
# answer "no" to the setuid-helper question and skip the interactive prompt.
Expand Down
51 changes: 49 additions & 2 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
with:
go-version-file: go.mod

- name: VERSION format
run: grep -Eqx '[0-9]{4}\.[0-9]{2}\.[0-9]{2}(\.[0-9]+)?' VERSION || { echo "::error file=VERSION::expected YYYY.MM.DD or YYYY.MM.DD.N, got '$(cat VERSION)'"; exit 1; }

- name: go vet
run: go vet ./...

Expand All @@ -34,6 +37,11 @@ jobs:
uses: dominikh/staticcheck-action@v1
with:
install-go: false
# v0.8.1 (latest release) can't read go1.27.2 export data ("export
# data version 5 is greater than maximum supported version 4").
# Pinned to a master commit verified against go1.27.2; switch
# back to a release tag once one newer than v0.8.1 ships.
version: "452d5bb86b45"

- name: govulncheck
run: go run golang.org/x/vuln/cmd/govulncheck@latest ./...
Expand Down Expand Up @@ -98,20 +106,30 @@ jobs:
- name: Checkout
uses: actions/checkout@v7

# Only a push needs credentials; pull requests from forks and Dependabot
# have no access to the secrets (same as dockette/.github docker.yml).
- name: Login to DockerHub
if: github.ref == 'refs/heads/master'
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Read VERSION
id: version
run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

# dockette/viewdoc:<variant> (rolling) + dockette/viewdoc:<variant>-<sha>
# (immutable, for rollbacks / pinning).
# dockette/viewdoc:<variant> rolling, latest master
# dockette/viewdoc:<variant>-<VERSION> release (YYYY.MM.DD); like other
# dockette versions it's rebuilt
# by the weekly schedule
# dockette/viewdoc:<variant>-<sha> immutable, for rollbacks / pinning
- name: Docker meta
id: meta
uses: docker/metadata-action@v6
Expand All @@ -120,7 +138,10 @@ jobs:
flavor: latest=false
tags: |
type=raw,value=${{ matrix.variant }}
type=raw,value=${{ matrix.variant }}-${{ steps.version.outputs.version }}
type=sha,prefix=${{ matrix.variant }}-,format=short
labels: |
org.opencontainers.image.version=${{ steps.version.outputs.version }}

- name: Build and push
uses: docker/build-push-action@v7
Expand All @@ -130,10 +151,36 @@ jobs:
push: ${{ github.ref == 'refs/heads/master' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha,scope=${{ matrix.variant }}
cache-to: type=gha,scope=${{ matrix.variant }}

# Tags the commit and publishes a GitHub release for VERSION the first
# time it reaches master with all images pushed (make release bumps it).
release:
name: "Release"
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Create release if VERSION is new
env:
GH_TOKEN: ${{ github.token }}
run: |
v="$(cat VERSION)"
if gh release view "$v" >/dev/null 2>&1; then
echo "Release $v already exists"
exit 0
fi
gh release create "$v" --target "$GITHUB_SHA" --title "$v" --generate-notes

docs:
name: "Docs"
needs: build
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/control-center
.DS_Store
.claude/worktrees/
Loading
Loading