diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6aefb0d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.github +.claude +.agents +.codex +docs +scripts +uploads +upgopher +coverage.out +*.log +.DS_Store diff --git a/.github/release-notes/v1.20.0.md b/.github/release-notes/v1.20.0.md new file mode 100644 index 0000000..fcef6c7 --- /dev/null +++ b/.github/release-notes/v1.20.0.md @@ -0,0 +1,77 @@ +# Upgopher v1.20.0 — easier to discover, install, and choose + +Upgopher v1.20.0 is an adoption release. The server's routes, flags, +authentication behavior, and file formats remain compatible with v1.19.1. + +## What is new + +- A shorter README centered on getting from a directory to a browser in one + command. +- A product website with installation, configuration, security, and + troubleshooting documentation. +- Project-maintained multi-architecture images on Docker Hub and GHCR. +- Project-maintained Homebrew and Scoop distribution. +- Refreshed product captures and an honest comparison with Python's + `http.server`, Dufs, File Browser, and Copyparty. +- Release automation that creates a draft for verification before publication. + +## Install + +```bash +# macOS +brew install --cask wanetty/tap/upgopher + +# Windows +scoop bucket add wanetty https://github.com/wanetty/scoop-bucket +scoop install upgopher + +# Docker +docker run --rm -p 9090:9090 -v "$PWD:/data" wanetty/upgopher:1.20.0 + +# With Go +go install github.com/wanetty/upgopher@latest +``` + +Release archives for Linux, macOS, and Windows are attached below. Verify a +download against `checksums.txt` before running it. + +## Compatibility + +No HTTP routes, flags, authentication behavior, or file formats changed. +Existing installations can keep their current command line and data directory. + +The Linux release archive names and embedded binary path remain compatible with +the Upgopher installer maintained by Proxmox Community Scripts. Existing LXC +instances continue using `/opt/upgopher/uploads` and their current systemd +service during updates. + +## Container notes + +The image now runs as the non-root UID/GID `65532`, serves `/data`, and keeps a +writable `/tmp` for temporary ZIP files. Bind-mounted directories must grant +that identity the permissions required by your chosen mode. + +Use the exact `1.20.0` tag for predictable deployments. `1.20`, `1`, and +`latest` are convenience tags that can move. + +## Intentional limits + +Upgopher remains a compact browser-sharing server. It does not add WebDAV, +resumable uploads, multi-user roles, storage backends, or a persistent +administration database in this release. + +## Validation + +- `make test` +- `make test-race` +- `make lint` +- `make build` +- GoReleaser v2 snapshot, archives, and checksums +- Linux `amd64` and `arm64` Community Scripts release contract +- Non-root container smoke tests for uploads, read-only mode, Basic Auth, TLS, + and ZIP downloads +- Desktop and mobile documentation checks + +See the [getting-started guide](https://wanetty.github.io/upgopher/getting-started.html) +and [security guide](https://wanetty.github.io/upgopher/security.html) before +exposing an instance beyond a trusted network. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1bb5886 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,82 @@ +name: CI + +on: + push: + branches: + - main + - dev + pull_request: + +permissions: + contents: read + +jobs: + test: + name: Go / ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.24.x" + cache: false + + - name: Test + run: go test ./... + + - name: Vet + run: go vet ./... + + - name: Build + run: go build -trimpath ./... + + race: + name: Race detector + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.24.x" + cache: false + + - name: Test with race detector + run: go test -race ./... + + release-contract: + name: Release / Community Scripts contract + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.24.x" + cache: false + + - name: Build release snapshot + uses: goreleaser/goreleaser-action@v7 + with: + distribution: goreleaser + version: "~> v2" + args: release --snapshot --clean --skip=publish + + - name: Verify Community Scripts compatibility + run: ./scripts/check-community-scripts-contract.sh diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 0000000..a2afb43 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,67 @@ +name: Container + +on: + push: + tags: + - "v*" + +permissions: + contents: read + packages: write + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate image metadata + id: metadata + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/wanetty/upgopher + wanetty/upgopher + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}} + type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }} + labels: | + org.opencontainers.image.title=Upgopher + org.opencontainers.image.description=A self-contained browser file and clipboard sharing server + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} + build-args: | + VERSION=${{ steps.metadata.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: mode=max + sbom: true diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml deleted file mode 100644 index a9068f7..0000000 --- a/.github/workflows/go.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Go - -on: - push: - tags: - - '*' - -permissions: - contents: write - - - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - name: Set up Go - uses: actions/setup-go@v3 - - name: GoReleaser Action - # You may pin to the exact commit or the version. - # uses: goreleaser/goreleaser-action@f82d6c1c344bcacabba2c841718984797f664a6b - uses: goreleaser/goreleaser-action@v4.2.0 - with: - # GoReleaser Distribution (goreleaser or goreleaser-pro) - distribution: goreleaser - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..366041b --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,40 @@ +name: Pages + +on: + push: + branches: + - main + paths: + - "docs/**" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload site + uses: actions/upload-pages-artifact@v3 + with: + path: docs + + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1e4ed0a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version: "1.24.x" + cache: false + + - name: Test + run: go test ./... + + - name: Vet + run: go vet ./... + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v7 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DISTRIBUTION_GITHUB_TOKEN: ${{ secrets.DISTRIBUTION_GITHUB_TOKEN }} + + - name: Checkout Scoop bucket + uses: actions/checkout@v5 + with: + repository: wanetty/scoop-bucket + token: ${{ secrets.DISTRIBUTION_GITHUB_TOKEN }} + path: scoop-bucket + + - name: Add Scoop update metadata + run: go run ./scripts/enrich-scoop-manifest ./scoop-bucket/upgopher.json + + - name: Publish Scoop update metadata + working-directory: scoop-bucket + run: | + if git diff --quiet -- upgopher.json; then + exit 0 + fi + git config user.name upgopher-release + git config user.email noreply@upgopher.dev + git add upgopher.json + git commit -m "chore: add Upgopher update metadata" + git push diff --git a/.gitignore b/.gitignore index 4d6d281..e346c5d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ *.so *.dylib upgopher +dist/ # Test binary, built with `go test -c` *.test @@ -16,5 +17,4 @@ upgopher # Dependency directories (remove the comment below to include it) # vendor/ uploads/ -.github/* -.agents/* \ No newline at end of file +.agents/ diff --git a/.goreleaser.yml b/.goreleaser.yml index 109266f..d9aa8b5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,12 +1,11 @@ -# .goreleaser.yml version: 2 -before: - hooks: - - go mod tidy +project_name: upgopher builds: - - id: "upgopher" + - id: upgopher + main: . + binary: upgopher env: - CGO_ENABLED=0 goos: @@ -15,19 +14,22 @@ builds: - windows goarch: - amd64 - - 386 - arm64 - goarm: - - '6' # For ARMv6 - - '7' # For ARMv7 + - "386" ignore: - goos: darwin - goarch: 386 # darwin 386 is not supported by Go anymore + goarch: "386" + flags: + - -trimpath ldflags: - - -s -w # Strip binary for smaller size + - -s -w -X main.version={{ .Version }} archives: - - name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + - id: upgopher + ids: + - upgopher + name_template: >- + {{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }} formats: - tar.gz format_overrides: @@ -38,17 +40,58 @@ archives: files: - LICENSE - README.md - + checksum: - name_template: 'checksums.txt' + name_template: checksums.txt algorithm: sha256 snapshot: - version_template: "{{ .Tag }}-next" + version_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: exclude: - - '^docs:' - - '^test:' + - "^docs:" + - "^test:" + +release: + draft: true + replace_existing_draft: true + name_template: "Upgopher {{ .Tag }}" + +homebrew_casks: + - name: upgopher + ids: + - upgopher + binaries: + - upgopher + homepage: https://wanetty.github.io/upgopher/ + description: Self-contained browser file and clipboard sharing server + license: MIT + commit_msg_template: "chore: update Upgopher to {{ .Tag }}" + repository: + owner: wanetty + name: homebrew-tap + branch: main + token: "{{ .Env.DISTRIBUTION_GITHUB_TOKEN }}" + commit_author: + name: upgopher-release + email: noreply@upgopher.dev + +scoops: + - name: upgopher + ids: + - upgopher + homepage: https://wanetty.github.io/upgopher/ + description: Self-contained browser file and clipboard sharing server + license: MIT + commit_msg_template: "chore: update Upgopher to {{ .Tag }}" + repository: + owner: wanetty + name: scoop-bucket + branch: main + token: "{{ .Env.DISTRIBUTION_GITHUB_TOKEN }}" + commit_author: + name: upgopher-release + email: noreply@upgopher.dev diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2218bc6..9312dbd 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,252 +1,104 @@ # Contributing to Upgopher -Thank you for your interest in contributing to Upgopher! This document provides guidelines and information about the project's architecture. +Thank you for helping improve Upgopher. The project favors small, reviewable +changes that preserve its security guarantees and self-contained binary. -## Project Architecture +## Development setup -Upgopher has been refactored from a monolithic structure to a modular architecture while maintaining **zero external dependencies** and **single binary distribution**. +Requirements: -### Directory Structure +- Go 1.19 or newer +- Make (optional) -``` -upgopher/ -├── upgopher.go # Main entry point (~200 lines) -├── upgopher_test.go # Unit tests -├── security_test.go # Security tests -├── Makefile # Development tasks -├── go.mod # Go module (zero dependencies) -├── internal/ -│ ├── security/ # Security functions -│ │ ├── path.go # Path traversal prevention -│ │ ├── ratelimit.go # Rate limiting -│ │ └── auth.go # HTTP Basic Auth -│ ├── utils/ # Utility functions -│ │ └── files.go # File operations (search, format size) -│ ├── templates/ # HTML generation -│ │ └── html.go # File/folder row templates -│ ├── app/ # Application state -│ │ └── app.go # App struct with config and state -│ ├── handlers/ # HTTP handlers (future) -│ ├── server/ # Routing (future) -│ └── statics/ # Embedded assets -│ ├── statics.go # Asset embedding -│ ├── templates/ # HTML templates -│ ├── css/ # Stylesheets -│ └── js/ # JavaScript -├── static/ # Static files (favicon, logo) -└── uploads/ # Default upload directory -``` - -### Key Design Principles - -1. **Zero Dependencies**: Only Go standard library -2. **Single Binary**: All assets embedded with `//go:embed` -3. **Thread-Safe**: All shared state protected by mutexes -4. **Security First**: Path traversal prevention, rate limiting, constant-time auth -5. **Testable**: >60% code coverage with comprehensive security tests - -### Package Descriptions - -#### `internal/security` -- **`path.go`**: `IsSafePath(baseDir, userPath)` - Prevents directory traversal attacks -- **`ratelimit.go`**: `CheckRateLimit(ip)` - 20 req/min per IP for clipboard endpoint -- **`auth.go`**: `ApplyBasicAuth(handler, user, pass)` - HTTP Basic Authentication wrapper - -#### `internal/utils` -- **`files.go`**: - - `FormatFileSize(size int64)` - Human-readable file sizes - - `SearchInFile(path, term, caseSensitive, wholeWord)` - Search within text files - - `SearchResult` type for search results - -#### `internal/templates` -- **`html.go`**: - - `CreateFileRow()` - Generate HTML for file entries - - `CreateFolderRow()` - Generate HTML for folder entries - - `CreateBackButton()`, `CreateZipButton()` - UI buttons - - `IsTextFile()` - Determine if file is readable as text - -#### `internal/app` -- **`app.go`**: - - `Config` struct - Server configuration - - `App` struct - Application state with thread-safe methods - - Methods: `GetCustomPath()`, `SetClipboard()`, `ToggleHiddenFiles()`, etc. - -## Development Workflow - -### Prerequisites -- Go 1.19 or higher -- Make (optional, but recommended) - -### Setup ```bash git clone https://github.com/wanetty/upgopher.git cd upgopher -go build +make build +make test-short ``` -### Available Make Targets +The main development commands are: + ```bash -make help # Show available commands -make build # Compile the project -make test # Run all tests -make test-race # Run tests with race detector -make test-coverage # Generate coverage report -make lint # Run static analysis -make clean # Remove binaries -make run # Build and run server -make ci # Full CI pipeline +make build # Build ./upgopher +make test # Run the full test suite +make test-short # Skip long-running tests +make test-race # Run tests with the race detector +make test-coverage # Write coverage.out and print coverage +make lint # Run go vet +make dev # Race detector and vet +make ci # Race detector, vet, and production build ``` -### Running Tests -```bash -# All tests -make test +## Architecture -# With race detection -make test-race +- `upgopher.go` parses flags, creates shared state, and starts HTTP or HTTPS. +- `internal/server/router.go` registers routes and applies optional Basic Auth. +- `internal/handlers/` contains file, clipboard, custom-path, and UI handlers. +- `internal/security/` contains path validation, auth, and rate limiting. +- `internal/statics/`, `static/`, and `//go:embed` keep runtime assets inside the + executable. +- `docs/` is the standalone GitHub Pages site and is not required at runtime. -# Only fast tests (skip 65s rate limit test) -go test -v -short ./... +## Non-negotiable constraints -# Specific test -go test -v -run TestIsSafePath -``` +- Use only the Go standard library; do not add modules to `go.mod`. +- Keep all runtime assets embedded in the executable. +- Preserve Windows, macOS, and Linux support. +- Call `security.IsSafePath(baseDir, fullPath)` before every filesystem read, + write, delete, or walk involving user-controlled paths. +- Keep user paths base64-encoded in URL query parameters. +- Preserve conditional Basic Auth registration and constant-time comparison. +- Do not expose absolute host paths in user-facing errors. +- Protect shared maps with their mutex; copy under lock before iterating when + practical. -### Code Style -- Follow standard Go formatting: `go fmt ./...` -- Run `go vet ./...` before committing -- Add tests for new features -- Security-critical code requires 100% test coverage - -## Making Changes - -### Adding New Features -1. **Create a feature branch**: `git checkout -b feature/your-feature` -2. **Write tests first**: TDD approach preferred -3. **Implement the feature**: Keep functions small and focused -4. **Update documentation**: README.md and code comments -5. **Run full test suite**: `make ci` -6. **Submit PR**: With clear description and test coverage - -### Security Guidelines -- **Always use `security.IsSafePath()`** before file operations -- **Never trust user input**: Validate and sanitize all inputs -- **Use constant-time comparisons** for authentication (already in `security.ApplyBasicAuth`) -- **Add security tests**: Test attack vectors in `security_test.go` - -### Modifying Existing Code -1. **Check test coverage**: `make test-coverage` -2. **Update affected tests**: Ensure they still pass -3. **Run race detector**: `make test-race` -4. **Test manually**: Build and run the server - -## Testing Guidelines - -### Test Categories -- **Unit Tests** (`upgopher_test.go`): Core functionality -- **Security Tests** (`security_test.go`): Attack vector validation -- **Integration Tests** (`internal/integration/`): End-to-end flows (future) - -### Writing Tests -```go -func TestYourFeature(t *testing.T) { - // Use t.TempDir() for temporary directories - tempDir := t.TempDir() - - // Table-driven tests preferred - tests := []struct { - name string - input string - want string - }{ - {"case 1", "input", "expected"}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := YourFunction(tt.input) - if got != tt.want { - t.Errorf("got %v, want %v", got, tt.want) - } - }) - } -} -``` +Read [AGENTS.md](./AGENTS.md) and +[the repository instructions](./.github/copilot-instructions.md) before making +code changes. -### Security Test Example -```go -func TestYourSecurityFeature(t *testing.T) { - attacks := []string{ - "../../../etc/passwd", - "..%2F..%2F..%2Fetc%2Fpasswd", - "/etc/passwd", - } - - for _, attack := range attacks { - if !isBlocked(attack) { - t.Errorf("Attack not blocked: %s", attack) - } - } -} -``` +## Pull requests -## Pull Request Process +1. Create a focused branch. +2. Add or update tests for changed behavior. +3. Run `gofmt` on changed Go files. +4. Run `make dev`; run `make ci` before requesting review. +5. Update the README, CLI help, or website when user-facing behavior changes. +6. Explain the behavior, security implications, and validation in the PR. -1. **Update tests**: Ensure all tests pass -2. **Update documentation**: README.md if user-facing changes -3. **Add changelog entry**: Update README.md changelog section -4. **Run full CI**: `make ci` must pass -5. **Request review**: Tag maintainers if needed -6. **Address feedback**: Be responsive to review comments +Use table-driven tests where useful and `t.TempDir()` for filesystem isolation. +Security-sensitive behavior should include explicit misuse or traversal cases. -## Release Process +## Documentation and website -Releases are automated via GoReleaser when a tag is pushed: +The public website is plain HTML, CSS, and JavaScript under `docs/`. It must +remain usable without a build step or third-party runtime dependency. + +To refresh the animated product demo after replacing its source screenshots: ```bash -# Tag a new version -git tag -a v1.12.0 -m "Release v1.12.0" -git push origin v1.12.0 - -# GitHub Actions will: -# - Run tests -# - Build binaries for all platforms -# - Create GitHub release -# - Attach binaries +go run ./scripts/create-demo-gif.go ``` -### Version Numbering -- **Major** (v2.0.0): Breaking changes -- **Minor** (v1.12.0): New features, backward compatible -- **Patch** (v1.11.1): Bug fixes only - -## Code Review Checklist +Check links, keyboard navigation, narrow viewports, reduced-motion behavior, and +image sizes before submitting documentation changes. -Before submitting a PR, verify: -- [ ] All tests pass (`make test`) -- [ ] No race conditions (`make test-race`) -- [ ] Code is formatted (`go fmt`) -- [ ] No lint errors (`make lint`) -- [ ] Documentation updated -- [ ] Changelog entry added -- [ ] Backward compatible (or breaking change documented) -- [ ] Security implications considered -- [ ] Single binary still works (`make build && ./upgopher -h`) +## Release process -## Getting Help +Pushing a `v*` tag starts independent GitHub Actions workflows: -- **Questions**: Open a GitHub issue with `question` label -- **Bug Reports**: Open an issue with steps to reproduce -- **Security Issues**: Contact [@gm_eduard](https://twitter.com/gm_eduard/) privately -- **Feature Requests**: Open an issue with `enhancement` label +- GoReleaser builds archives and checksums, updates the project Homebrew tap and + Scoop bucket, and creates a draft GitHub release. +- The container workflow publishes multi-architecture images to GHCR and Docker + Hub. -## Code of Conduct +The draft is published manually only after following +[`distribution/release-checklist.md`](./distribution/release-checklist.md). +Maintainers must never put distribution tokens in the repository or logs. -- Be respectful and inclusive -- Focus on constructive feedback -- Help others learn and grow -- Prioritize security and user safety +## Security reports -## License +Do not open public issues for suspected vulnerabilities. Follow +[SECURITY.md](./SECURITY.md) to coordinate a private report. -By contributing, you agree that your contributions will be licensed under the MIT License. +Contributions are licensed under the [MIT License](./LICENSE). diff --git a/Dockerfile b/Dockerfile index b8030c0..393d94e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,36 @@ -FROM docker.io/golang:1.21-alpine AS build +# syntax=docker/dockerfile:1 + +FROM docker.io/library/golang:1.24-alpine3.21 AS build + +ARG TARGETOS=linux +ARG TARGETARCH=amd64 +ARG VERSION=dev + WORKDIR /src -COPY go.mod ./ -RUN go mod download +COPY go.mod ./ COPY . . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o upgopher . +RUN CGO_ENABLED=0 GOOS="${TARGETOS}" GOARCH="${TARGETARCH}" \ + go build -trimpath -ldflags="-s -w -X main.version=${VERSION}" -o /out/upgopher . +RUN mkdir -p /rootfs/data /rootfs/tmp \ + && chown -R 65532:65532 /rootfs \ + && chmod 1777 /rootfs/tmp -FROM alpine:latest +FROM scratch -WORKDIR /app -COPY --from=build /src/upgopher . -RUN mkdir uploads +LABEL org.opencontainers.image.title="Upgopher" \ + org.opencontainers.image.description="A self-contained browser file and clipboard sharing server" \ + org.opencontainers.image.source="https://github.com/wanetty/upgopher" \ + org.opencontainers.image.licenses="MIT" + +COPY --from=build --chown=65532:65532 /out/upgopher /upgopher +COPY --from=build --chown=65532:65532 /rootfs/data /data +COPY --from=build --chown=65532:65532 /rootfs/tmp /tmp + +USER 65532:65532 +WORKDIR /data +VOLUME ["/data"] EXPOSE 9090 -CMD ["./upgopher"] \ No newline at end of file + +ENTRYPOINT ["/upgopher"] +CMD ["-dir", "/data"] diff --git a/README.md b/README.md index 1f9abcf..f47b425 100644 --- a/README.md +++ b/README.md @@ -1,201 +1,149 @@ # Upgopher -

Logo

+

+ Upgopher, a gopher carrying files +

-[![Go](https://github.com/wanetty/upgopher/actions/workflows/go.yml/badge.svg)](https://github.com/wanetty/upgopher/actions/workflows/go.yml) +

+ A single self-contained binary to share files and clipboard content between devices—no database, dependencies, or mandatory configuration. +

-Upgopher is a zero-dependency Go web server for file sharing. It provides a browser-based interface for uploading, downloading, and managing files, with optional HTTPS, basic authentication, and read-only mode. Distributed as a single self-contained binary with no external runtime dependencies. +

+ CI status + Latest release + MIT license + Container image +

-![Example Photo](./static/example.png) -![Example Photo 2](./static/example2.png) -![Directory Tree](./static/directorytree_exmaple.png) +

+ + + Upgopher demo: browse files, upload content, and share clipboard text + +

-## Features -* Users can upload files by selecting a file and clicking the "Upload" button -* Uploaded files are stored in the "uploads" directory by default, but the directory can be changed using the -dir flag -* Users can view a list of the uploaded files by visiting the root URL -* Basic authentication is available to restrict access to the server. To use it, set the -user and -pass flags with the desired username and password. -* Traffic via HTTPS with self-signed certificate generation or custom certificates -* Browse through folders and upload files with drag-and-drop support -* Directory tree sidebar with expand/collapse controls -* Breadcrumb navigation with clickable path segments -* Copy file URLs to clipboard with one click for easy sharing -* Search within text files directly from the web interface -* Create custom path aliases for easy file access -* Shared clipboard for cross-device text and screenshot sharing -* Zip folder download functionality -* Option to hide hidden files with the -disable-hidden-files flag -* Readonly mode to disable uploads and deletions while allowing downloads +## Start sharing in three steps - - -## Installation - - -### Automatically - -Just run this command in your terminal with go installed. ```bash +# 1. Install go install github.com/wanetty/upgopher@latest -``` -### Releases +# 2. Share a directory +upgopher -dir ./shared -Go to the [releases](https://github.com/wanetty/upgopher/releases) section and get the one you need. - -### Manual - -Just build it yourself - -```bash -git clone https://github.com/wanetty/upgopher.git -cd upgopher -go build +# 3. Open http://localhost:9090 on this or another device ``` -### Docker -```bash -docker build . -t upgopher -docker run --name upgopher -p 9090:9090 upgopher -``` +Upgopher binds to all interfaces by default. Use Basic Auth, a trusted local +network, a VPN, or a properly configured reverse proxy before exposing it beyond +your machine. -## Usage +## Install -### Help Output: +| Method | Command | +| --- | --- | +| Homebrew | `brew install --cask wanetty/tap/upgopher` | +| Scoop | `scoop bucket add wanetty https://github.com/wanetty/scoop-bucket` then `scoop install upgopher` | +| Docker | `docker run --rm -p 9090:9090 -v "$PWD:/data" wanetty/upgopher:1.20.0` | +| Go | `go install github.com/wanetty/upgopher@latest` | +| Binary | Download the archive for your OS from [GitHub Releases](https://github.com/wanetty/upgopher/releases/latest) and verify it with `checksums.txt` | -```bash -./upgopher -h -Usage of ./upgopher: - -cert string - HTTPS certificate - -dir string - directory path (default "./uploads") - -disable-hidden-files - disable showing hidden files - -key string - private key for HTTPS - -max-upload-size int - maximum upload size in GB (0 means unlimited) - -max-tabs int - maximum number of shared clipboard tabs - -pass string - password for authentication - -port int - port number (default 9090) - -q quiet mode - -read-timeout duration - server read timeout (0 means unlimited) - -read-header-timeout duration - server read header timeout - -write-timeout duration - server write timeout (0 means unlimited) - -readonly - readonly mode (disable upload and delete operations) - -ssl - use HTTPS on port 443 by default. (If you don't put cert and key, it will generate a self-signed certificate) - -user string -``` +The project-maintained container is also published as +`ghcr.io/wanetty/upgopher`. See the +[installation guide](https://wanetty.github.io/upgopher/getting-started.html) +for platform-specific instructions and persistent Docker examples. -### Examples +## What it is good at -**Basic usage:** -```bash -./upgopher -``` -This will start the server on the default port (9090) and store uploaded files in the ./uploads directory. +- **Move files across a LAN.** Open a browser on a phone, laptop, VM, or lab + machine and upload or download without setting up a client. +- **Share text and screenshots between devices.** The built-in shared clipboard + supports multiple tabs and optional per-tab protection. +- **Expose a development folder temporarily.** Serve build output, logs, or test + artifacts with one command. +- **Publish a read-only download portal.** Keep browsing and downloads available + while disabling uploads and deletion with `-readonly`. +- **Run a small homelab file drop.** Put Upgopher behind your VPN, tunnel, or + reverse proxy when you need a compact browser-based tool instead of a storage + platform. -**Custom port and directory:** -```bash -./upgopher -port 8080 -dir "/path/to/files" -``` +## Included -**With basic authentication:** -```bash -./upgopher -user admin -pass secretpassword -``` +**Files:** drag-and-drop file and folder uploads, download, directory creation +and deletion, ZIP downloads, search inside text files, breadcrumbs, and a +directory tree. -**With HTTPS (self-signed certificate):** -```bash -./upgopher -ssl -``` +**Sharing:** copyable direct links, custom path aliases, and a shared clipboard +for text and screenshots. -**With HTTPS (custom certificate):** -```bash -./upgopher -ssl -cert /path/to/cert.pem -key /path/to/key.pem -``` +**Controls:** optional Basic Auth, built-in HTTPS with a self-signed or custom +certificate, read-only mode, hidden-file controls, upload limits, and server +timeouts. -**Hide hidden files:** -```bash -./upgopher -disable-hidden-files -``` +Everything required by the UI is embedded in the binary. Upgopher uses only the +Go standard library and does not require a database or configuration file. -**Readonly mode (disable uploads and deletions):** -```bash -./upgopher -readonly -``` +## Choosing the right tool -**Limit upload size to 1 GB:** -```bash -./upgopher -max-upload-size 1 -``` +Comparison checked for Upgopher v1.20.0 in July 2026. Projects evolve; follow +the linked sources for their current feature sets. -**Limit shared clipboard tabs to 5:** -```bash -./upgopher -max-tabs 5 -``` +| Tool | Artifact / runtime | Upload and management | Auth model | Users / roles | HTTPS | WebDAV | Resumable uploads | Shared clipboard | Best fit | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| **Upgopher** | Self-contained Go binary | Browser file manager | Optional global Basic Auth | No | Built in | No | No | Text and screenshots | The smallest step from a folder to a practical browser sharing UI | +| [`python -m http.server`](https://docs.python.org/3/library/http.server.html) | Python runtime | Browse and download | No built-in access control | No | CLI TLS options in current Python | No | No | No | Temporary, read-only serving during local development | +| [Dufs](https://github.com/sigoden/dufs) | Single Rust binary | Upload, edit, search, archive | Access controls | Account rules | Built in | Yes | Yes | No | A compact server when WebDAV or resumable transfers matter | +| [File Browser](https://filebrowser.org/) | Binary or container with persistent state | Full web file administration | Login and persistent auth | Yes | Commonly via proxy | No | No | No | Multi-user file management with administration and permissions | +| [Copyparty](https://github.com/9001/copyparty) | Python runtime or self-extracting bundle | Extensive file and media features | Accounts and per-volume permissions | Yes | Built in | Yes | Yes | No | Advanced transfers, protocols, indexing, deduplication, and media workflows | -**Set a custom read timeout (for large uploads on slower links):** -```bash -./upgopher -read-timeout 30m -``` +Upgopher deliberately does **not** provide WebDAV, resumable uploads, +multi-user roles, storage backends, or a persistent administration database. If +one of those is central to your setup, one of the alternatives above is likely +a better choice. -**Recommended for large uploads:** -```bash -./upgopher -read-timeout 0 -write-timeout 0 -``` +## Common commands -Note: Cloudflare Quick Tunnels are intended for testing and can impose limits. For reliable large uploads, prefer a full Cloudflare Tunnel. - - -## Security - -### Reporting Vulnerabilities - -If you discover a security vulnerability, please contact [@gm_eduard](https://twitter.com/gm_eduard/) directly. Please do not open a public issue. - - -## License -This project is licensed under the MIT License. See the LICENSE file for details. +```bash +# Protect access +upgopher -dir ./shared -user admin -pass 'choose-a-strong-password' -## Development +# Download-only portal +upgopher -dir ./public -readonly -### Building from Source +# Built-in HTTPS with an ephemeral self-signed certificate +upgopher -dir ./shared -ssl -```bash -git clone https://github.com/wanetty/upgopher.git -cd upgopher -go build -o upgopher +# Custom port and a 1 GiB upload limit +upgopher -dir ./shared -port 8080 -max-upload-size 1 ``` -### Running Tests +Read the full [configuration reference](https://wanetty.github.io/upgopher/configuration.html). -```bash -# All tests -go test -v ./... +## Security and limits -# Only fast tests (skip time-based tests) -go test -v -short ./... +- Treat an unauthenticated instance as public to every device that can reach + its port. +- Prefer a VPN or a reverse proxy with trusted TLS for internet-facing use. +- `-ssl` without `-cert` and `-key` creates a new self-signed certificate at + startup; clients will not trust it automatically. +- Basic Auth is global. Upgopher does not provide separate user accounts or + per-folder permissions. +- Clipboard data and custom path aliases are held in memory and disappear when + the process restarts. +- A read-only instance prevents uploads and deletion through Upgopher; it does + not change host filesystem permissions. -# With coverage -go test -cover ./... -``` +See the [security guide](https://wanetty.github.io/upgopher/security.html). +Report vulnerabilities privately to [@gm_eduard](https://twitter.com/gm_eduard); +do not open a public issue. + +## Documentation -### Contributing +- [Website and overview](https://wanetty.github.io/upgopher/) +- [Getting started](https://wanetty.github.io/upgopher/getting-started.html) +- [Configuration reference](https://wanetty.github.io/upgopher/configuration.html) +- [Security](https://wanetty.github.io/upgopher/security.html) +- [Troubleshooting](https://wanetty.github.io/upgopher/troubleshooting.html) +- [Contributing](./CONTRIBUTING.md) -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Make your changes -4. Run tests (`go test -v ./...`) -5. Commit your changes (`git commit -m 'Add amazing feature'`) -6. Push to the branch (`git push origin feature/amazing-feature`) -7. Open a Pull Request +Upgopher is available under the [MIT License](./LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..7720afd --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security policy + +## Supported versions + +Security fixes are provided for the latest released version of Upgopher. +Reproduce a report against that version when possible. + +## Reporting a vulnerability + +Contact [@gm_eduard](https://twitter.com/gm_eduard/) privately. Include: + +- the affected Upgopher version and operating system; +- a minimal reproduction; +- the expected and observed behavior; +- the security impact; +- logs or screenshots with credentials and private paths removed. + +Do not open a public issue before disclosure has been coordinated. You should +receive an acknowledgement within seven days. Timelines for validation, a fix, +and public disclosure will depend on severity and reproducibility. + +Please do not access data that is not yours, disrupt third-party systems, or +retain sensitive data while researching a report. diff --git a/distribution/README.md b/distribution/README.md new file mode 100644 index 0000000..ebcc42e --- /dev/null +++ b/distribution/README.md @@ -0,0 +1,34 @@ +# Distribution repositories and secrets + +Upgopher publishes release metadata to two project-owned repositories: + +- `wanetty/homebrew-tap` +- `wanetty/scoop-bucket` + +Create both as public repositories with a `main` branch before pushing the +v1.20.0 tag. The release workflow writes generated files directly to that +branch. + +## Required GitHub Actions secrets + +Configure these in `wanetty/upgopher`: + +| Secret | Scope | +| --- | --- | +| `DISTRIBUTION_GITHUB_TOKEN` | Fine-grained token with repository Contents read/write for `homebrew-tap` and `scoop-bucket` only | +| `DOCKERHUB_USERNAME` | Docker Hub account name | +| `DOCKERHUB_TOKEN` | Docker Hub access token with permission to update `wanetty/upgopher` | + +The built-in `GITHUB_TOKEN` publishes release assets and the GHCR image. Do not +reuse a personal password or a broadly scoped classic token. + +Initialize `homebrew-tap` with a short README and initialize `scoop-bucket` with +a short README. GoReleaser creates the Cask and Scoop manifest on the first +tagged release. The release workflow then adds Scoop `checkver` and +`autoupdate` metadata while preserving GoReleaser's architecture-specific +SHA-256 hashes. + +## GitHub Pages + +In repository settings, select **GitHub Actions** as the Pages source. The +`Pages` workflow publishes the contents of `/docs` on changes to `main`. diff --git a/distribution/adoption-baseline.md b/distribution/adoption-baseline.md new file mode 100644 index 0000000..c05b363 --- /dev/null +++ b/distribution/adoption-baseline.md @@ -0,0 +1,20 @@ +# Adoption baseline + +Capture platform-native metrics immediately before publishing v1.20.0, then +repeat the same fields after 7 and 30 days. Do not add third-party analytics to +the website. + +| Metric | Pre-release | Day 7 | Day 30 | +| --- | ---: | ---: | ---: | +| GitHub stars | 65 (observed 2026-07-29) | | | +| GitHub forks | 4 (observed 2026-07-29) | | | +| GitHub clones, previous 14 days | Record at release | | | +| GitHub release downloads | 0 for v1.20.0 | | | +| Docker Hub pulls | Record at release | | | +| GHCR downloads | Record at release | | | +| Homebrew installs | Record when available | | | +| Scoop installs | Not exposed centrally; note support signals | | | + +Also record the release publication timestamp, website publication timestamp, +and awesome-selfhosted pull request URL so that changes can be interpreted +against distribution events. diff --git a/distribution/awesome-selfhosted.md b/distribution/awesome-selfhosted.md new file mode 100644 index 0000000..ff0abe5 --- /dev/null +++ b/distribution/awesome-selfhosted.md @@ -0,0 +1,24 @@ +# awesome-selfhosted submission + +Target category: **File Transfer & Synchronization** + +Proposed description (184 characters): + +> Self-contained cross-platform file server with a browser UI for uploads, +> downloads, folder management, a shared text/image clipboard, optional Basic +> Auth and HTTPS, and read-only mode. + +Suggested metadata: + +- Website: `https://wanetty.github.io/upgopher/` +- Source: `https://github.com/wanetty/upgopher` +- License: `MIT` +- Language/platform: `Go` +- Container: `ghcr.io/wanetty/upgopher:1.20.0` + +Before opening the pull request, check the current contribution template and +machine-readable schema in +[`awesome-selfhosted/awesome-selfhosted`](https://github.com/awesome-selfhosted/awesome-selfhosted). +Search open and closed pull requests for Upgopher, then use the repository's +generator or validation command rather than manually guessing its current data +format. diff --git a/distribution/release-checklist.md b/distribution/release-checklist.md new file mode 100644 index 0000000..7ac65d7 --- /dev/null +++ b/distribution/release-checklist.md @@ -0,0 +1,51 @@ +# v1.20.0 release checklist + +## Before tagging + +- [ ] `make test`, `make test-race`, `make lint`, and `make build` pass. +- [ ] `goreleaser check` passes with the current GoReleaser v2. +- [ ] `scripts/check-community-scripts-contract.sh` passes against a release + snapshot, preserving the Linux `amd64`/`arm64` archive names and embedded + `upgopher` path consumed by the Proxmox Community Script. +- [ ] All links in README and `/docs` resolve. +- [ ] The website is checked at desktop and mobile widths with keyboard-only + navigation and reduced motion. +- [ ] `docs/assets/upgopher-demo.gif` is below 10 MB and contains no private + data or paths. +- [ ] `wanetty/homebrew-tap` and `wanetty/scoop-bucket` exist and the + distribution token can write only to those repositories. +- [ ] Docker Hub contains the public `wanetty/upgopher` repository and its token + is configured. +- [ ] GitHub Pages uses GitHub Actions as its source. +- [ ] Copy `.github/release-notes/v1.20.0.md` into the release draft body. + +## Create the draft + +```bash +git tag -a v1.20.0 -m "Upgopher v1.20.0" +git push origin v1.20.0 +``` + +Do not publish the draft until both release workflows finish successfully. + +## Verify artifacts + +- [ ] Linux, macOS, and Windows archives exist for each configured architecture. +- [ ] `checksums.txt` matches downloaded sample archives. +- [ ] Extracted binaries run with `-h`. +- [ ] An existing Community Scripts LXC updates to v1.20.0 without changing + `/opt/upgopher/uploads` or `/etc/systemd/system/upgopher.service`. +- [ ] Homebrew installs and uninstalls on macOS arm64 and amd64. +- [ ] Scoop installs, updates, and uninstalls on Windows amd64 and arm64. +- [ ] Both container registries expose `1.20.0`, `1.20`, `1`, and `latest`. +- [ ] `linux/amd64` and `linux/arm64` images start as UID/GID `65532`. +- [ ] Container file upload, read-only mode, Basic Auth, custom TLS, and ZIP + download work with a bind-mounted `/data`. +- [ ] The website and README installation commands point to public artifacts. + +## Publish and follow up + +- [ ] Publish the GitHub release. +- [ ] Submit the prepared awesome-selfhosted entry. +- [ ] Record the publication timestamp in `adoption-baseline.md`. +- [ ] Record the same metrics after 7 and 30 days. diff --git a/docs/.nojekyll b/docs/.nojekyll new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/.nojekyll @@ -0,0 +1 @@ + diff --git a/docs/assets/clipboard.png b/docs/assets/clipboard.png new file mode 100644 index 0000000..efc2191 Binary files /dev/null and b/docs/assets/clipboard.png differ diff --git a/docs/assets/directory-tree.png b/docs/assets/directory-tree.png new file mode 100644 index 0000000..c488211 Binary files /dev/null and b/docs/assets/directory-tree.png differ diff --git a/docs/assets/favicon.ico b/docs/assets/favicon.ico new file mode 100644 index 0000000..c7da3f8 Binary files /dev/null and b/docs/assets/favicon.ico differ diff --git a/docs/assets/file-manager.png b/docs/assets/file-manager.png new file mode 100644 index 0000000..8d654bf Binary files /dev/null and b/docs/assets/file-manager.png differ diff --git a/docs/assets/logopher.webp b/docs/assets/logopher.webp new file mode 100644 index 0000000..69b46e7 Binary files /dev/null and b/docs/assets/logopher.webp differ diff --git a/docs/assets/mobile.png b/docs/assets/mobile.png new file mode 100644 index 0000000..d76918b Binary files /dev/null and b/docs/assets/mobile.png differ diff --git a/docs/assets/site.css b/docs/assets/site.css new file mode 100644 index 0000000..1db63de --- /dev/null +++ b/docs/assets/site.css @@ -0,0 +1,716 @@ +:root { + --paper: #f5f5f5; + --paper-deep: #eef7f2; + --ink: #333333; + --ink-soft: #5f6b66; + --signal: #009879; + --signal-dark: #3a8c5b; + --moss: #45bc75; + --line: #dddddd; + --white: #ffffff; + --shadow: 0 12px 34px rgba(0, 0, 0, 0.1); + --max: 1160px; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + color: var(--ink); + background: var(--paper); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + line-height: 1.65; +} + +body::before { + content: ""; + position: fixed; + width: 520px; + height: 520px; + top: -230px; + right: -160px; + z-index: -1; + pointer-events: none; + border-radius: 50%; + background: rgba(0, 152, 121, 0.075); +} + +a { + color: var(--signal-dark); + text-decoration-thickness: 0.09em; + text-underline-offset: 0.16em; +} + +a:hover { + color: var(--signal); +} + +img { + max-width: 100%; +} + +.skip-link { + position: absolute; + left: 1rem; + top: -5rem; + z-index: 20; + padding: 0.7rem 1rem; + color: var(--white); + background: var(--ink); +} + +.skip-link:focus { + top: 1rem; +} + +.site-header { + position: sticky; + top: 0; + z-index: 10; + border-bottom: 1px solid var(--line); + background: rgba(245, 245, 245, 0.94); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + backdrop-filter: blur(10px); +} + +.nav { + width: min(calc(100% - 2rem), var(--max)); + min-height: 70px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.75rem; + color: var(--ink); + font-family: inherit; + font-size: 1.35rem; + font-weight: 700; + text-decoration: none; +} + +.brand img { + width: 38px; + height: 38px; + object-fit: contain; +} + +.nav-links { + display: flex; + align-items: center; + gap: 1.4rem; + font-size: 0.88rem; + font-weight: 650; +} + +.nav-links a { + color: var(--ink); + text-decoration: none; +} + +.nav-links a[aria-current="page"], +.nav-links a:hover { + color: var(--signal-dark); +} + +.nav-toggle { + display: none; + border: 1px solid var(--line); + border-radius: 8px; + padding: 0.55rem 0.7rem; + color: var(--ink); + background: var(--white); + font: 700 0.8rem inherit; +} + +.wrap { + width: min(calc(100% - 2rem), var(--max)); + margin: 0 auto; +} + +.hero { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(360px, 0.95fr); + gap: clamp(2rem, 6vw, 6rem); + align-items: center; + min-height: calc(100vh - 70px); + padding: 5rem 0; +} + +.eyebrow, +.section-label { + display: inline-flex; + align-items: center; + gap: 0.6rem; + margin: 0 0 1rem; + color: var(--signal-dark); + font: 750 0.78rem ui-monospace, "SFMono-Regular", Consolas, monospace; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.eyebrow::before, +.section-label::before { + content: ""; + width: 28px; + height: 3px; + background: var(--signal); +} + +h1, +h2, +h3 { + font-family: inherit; + line-height: 1.08; +} + +h1 { + max-width: 760px; + margin: 0; + font-size: clamp(3.2rem, 8.5vw, 7rem); + font-weight: 780; + letter-spacing: -0.055em; +} + +h1 em { + color: var(--signal-dark); + font-style: normal; + font-weight: 780; +} + +.hero-copy { + max-width: 650px; + margin: 1.6rem 0 0; + color: var(--ink-soft); + font-size: clamp(1.05rem, 2vw, 1.3rem); +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; + margin-top: 2rem; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 48px; + border: 1px solid var(--signal); + border-radius: 8px; + padding: 0.72rem 1.1rem; + color: var(--white); + background: var(--signal); + box-shadow: 0 3px 10px rgba(0, 152, 121, 0.2); + font: 700 0.82rem inherit; + text-decoration: none; + transition: transform 140ms ease, box-shadow 140ms ease, background 140ms ease; +} + +.button.secondary { + color: var(--ink); + background: var(--white); + border-color: var(--line); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); +} + +.button:hover { + transform: translateY(-1px); + color: var(--white); + background: var(--signal-dark); + box-shadow: 0 6px 14px rgba(0, 152, 121, 0.24); +} + +.button.secondary:hover { + color: var(--ink); + background: #f9f9f9; + box-shadow: 0 5px 14px rgba(0, 0, 0, 0.09); +} + +.hero-card { + position: relative; + border: 1px solid var(--line); + border-radius: 16px; + padding: 1rem; + background: var(--white); + box-shadow: var(--shadow); +} + +.hero-card::before { + content: "LIVE TRANSFER / :9090"; + display: block; + margin-bottom: 0.8rem; + color: var(--signal-dark); + font: 800 0.72rem ui-monospace, monospace; + letter-spacing: 0.08em; +} + +.hero-card img { + display: block; + width: 100%; + border: 1px solid var(--line); + border-radius: 10px; +} + +.proof-strip { + background: #173c32; + color: var(--white); +} + +.proof-list { + display: grid; + grid-template-columns: repeat(4, 1fr); +} + +.proof-item { + padding: 1.2rem; + border-right: 1px solid #627078; + text-align: center; +} + +.proof-item:last-child { + border-right: 0; +} + +.proof-item strong { + display: block; + color: #7ae3ad; + font: 800 1.05rem ui-monospace, monospace; +} + +.proof-item span { + font-size: 0.84rem; +} + +.section { + padding: clamp(4.5rem, 9vw, 8rem) 0; +} + +.section.alt { + border-block: 1px solid #dce9e2; + background: var(--paper-deep); +} + +.section-head { + display: grid; + grid-template-columns: minmax(0, 0.8fr) minmax(280px, 1.2fr); + gap: 2rem; + align-items: end; + margin-bottom: 3rem; +} + +.section h2, +.doc-hero h1 { + margin: 0; + font-size: clamp(2.5rem, 6vw, 4.8rem); + letter-spacing: -0.045em; +} + +.section-intro { + max-width: 650px; + margin: 0; + color: var(--ink-soft); + font-size: 1.1rem; +} + +.use-grid, +.feature-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.card { + position: relative; + min-height: 220px; + border: 1px solid var(--line); + border-radius: 12px; + padding: 1.5rem; + background: var(--white); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.07); +} + +.card:nth-child(2n) { + transform: translateY(0.7rem); +} + +.card .number { + color: var(--signal-dark); + font: 800 0.82rem ui-monospace, monospace; +} + +.card h3 { + margin: 2.2rem 0 0.7rem; + font-size: 1.55rem; +} + +.card p { + margin: 0; + color: var(--ink-soft); +} + +.terminal { + overflow: hidden; + border: 1px solid #285c4d; + border-radius: 12px; + background: #173c32; + color: #eff5e9; + box-shadow: var(--shadow); +} + +.terminal-head { + display: flex; + align-items: center; + gap: 0.45rem; + border-bottom: 1px solid #435058; + padding: 0.75rem 1rem; + color: #a8b5b8; + font: 0.75rem ui-monospace, monospace; +} + +.terminal-head i { + width: 10px; + height: 10px; + border-radius: 50%; + background: #7ae3ad; +} + +.terminal pre { + overflow-x: auto; + margin: 0; + padding: 1.4rem; + font: 0.88rem/1.8 ui-monospace, "SFMono-Regular", Consolas, monospace; +} + +.terminal .prompt { + color: #7ae3ad; +} + +.install-layout { + display: grid; + grid-template-columns: 0.7fr 1.3fr; + gap: clamp(2rem, 6vw, 6rem); + align-items: center; +} + +.install-tabs { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + margin-bottom: 0.9rem; +} + +.install-tab { + border: 1px solid #647178; + padding: 0.5rem 0.7rem; + color: #b8c2c4; + background: transparent; + font: 700 0.75rem ui-monospace, monospace; + cursor: pointer; +} + +.install-tab[aria-selected="true"] { + border-color: #7ae3ad; + color: #173c32; + background: #7ae3ad; +} + +.install-panel[hidden] { + display: none; +} + +.shot-grid { + display: grid; + grid-template-columns: 1.25fr 0.75fr; + gap: 1rem; +} + +.shot { + border: 1px solid var(--line); + border-radius: 12px; + padding: 0.65rem; + background: var(--white); + box-shadow: 0 5px 18px rgba(0, 0, 0, 0.08); +} + +.shot img { + display: block; + width: 100%; + border: 1px solid var(--line); + border-radius: 8px; +} + +.shot figcaption { + padding: 0.65rem 0.2rem 0.1rem; + color: var(--ink-soft); + font: 0.72rem ui-monospace, monospace; + text-transform: uppercase; +} + +.table-wrap { + overflow-x: auto; + border: 1px solid var(--line); + border-radius: 12px; + background: var(--white); + box-shadow: 0 4px 15px rgba(0, 0, 0, 0.06); +} + +table { + width: 100%; + min-width: 760px; + border-collapse: collapse; +} + +th, +td { + border-bottom: 1px solid var(--line); + padding: 0.9rem 1rem; + text-align: left; + vertical-align: top; +} + +th { + color: var(--white); + background: var(--signal); + font: 800 0.75rem ui-monospace, monospace; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +tr:last-child td { + border-bottom: 0; +} + +tr.highlight td { + background: #e9f7f0; + font-weight: 650; +} + +.doc-hero { + padding: 6rem 0 3rem; + border-bottom: 1px solid var(--line); + background: var(--paper-deep); +} + +.doc-hero p { + max-width: 720px; + color: var(--ink-soft); + font-size: 1.15rem; +} + +.doc-layout { + display: grid; + grid-template-columns: 220px minmax(0, 760px); + gap: 4rem; + align-items: start; + padding: 4rem 0 7rem; +} + +.toc { + position: sticky; + top: 100px; + border-left: 3px solid var(--signal); + padding-left: 1rem; +} + +.toc strong { + font: 800 0.74rem ui-monospace, monospace; + text-transform: uppercase; +} + +.toc a { + display: block; + margin-top: 0.65rem; + color: var(--ink-soft); + font-size: 0.9rem; + text-decoration: none; +} + +.doc-content h2 { + margin: 3.5rem 0 1rem; + font-size: 2.15rem; +} + +.doc-content h2:first-child { + margin-top: 0; +} + +.doc-content h3 { + margin: 2rem 0 0.6rem; + font-size: 1.35rem; +} + +.doc-content code:not(pre code) { + border: 1px solid var(--line); + padding: 0.08rem 0.32rem; + background: var(--white); + font: 0.88em ui-monospace, monospace; +} + +.doc-content pre { + overflow-x: auto; + border-left: 5px solid var(--signal); + padding: 1rem 1.2rem; + color: #eff5e9; + border-radius: 0 8px 8px 0; + background: #173c32; + font: 0.88rem/1.7 ui-monospace, monospace; +} + +.callout { + margin: 1.5rem 0; + border: 1px solid #b8ddc9; + border-radius: 10px; + padding: 1rem 1.2rem; + background: #e9f7f0; + box-shadow: 0 3px 12px rgba(0, 0, 0, 0.06); +} + +.callout strong { + font-family: ui-monospace, monospace; + text-transform: uppercase; +} + +.cta { + display: grid; + grid-template-columns: 1fr auto; + gap: 2rem; + align-items: center; + border: 0; + border-radius: 16px; + padding: clamp(1.8rem, 5vw, 3.5rem); + background: var(--signal); + box-shadow: var(--shadow); +} + +.cta h2 { + max-width: 720px; + color: var(--white); +} + +.site-footer { + border-top: 0; + padding: 2.5rem 0; + background: #173c32; + color: #ccd4d5; +} + +.footer-inner { + display: flex; + justify-content: space-between; + gap: 2rem; + align-items: center; +} + +.site-footer a { + color: #7ae3ad; +} + +@media (max-width: 900px) { + .hero, + .section-head, + .install-layout { + grid-template-columns: 1fr; + } + + .hero { + min-height: auto; + } + + .hero-card { + max-width: 700px; + } + + .proof-list { + grid-template-columns: repeat(2, 1fr); + } + + .proof-item:nth-child(2) { + border-right: 0; + } + + .proof-item:nth-child(-n + 2) { + border-bottom: 1px solid #627078; + } + + .use-grid, + .feature-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .doc-layout { + grid-template-columns: 1fr; + } + + .toc { + position: static; + } +} + +@media (max-width: 680px) { + .nav-toggle { + display: block; + } + + .nav-links { + position: absolute; + inset: 70px 0 auto; + display: none; + align-items: stretch; + border-bottom: 1px solid var(--line); + padding: 1rem; + background: var(--paper); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.08); + } + + .nav-links.open { + display: grid; + } + + h1 { + font-size: clamp(3rem, 18vw, 5rem); + } + + .use-grid, + .feature-grid, + .shot-grid, + .cta { + grid-template-columns: 1fr; + } + + .card:nth-child(2n) { + transform: none; + } + + .proof-list { + grid-template-columns: 1fr 1fr; + } + + .footer-inner { + display: block; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + + *, + *::before, + *::after { + transition-duration: 0.01ms !important; + } +} diff --git a/docs/assets/site.js b/docs/assets/site.js new file mode 100644 index 0000000..84494d0 --- /dev/null +++ b/docs/assets/site.js @@ -0,0 +1,23 @@ +(function () { + var toggle = document.querySelector(".nav-toggle"); + var links = document.querySelector(".nav-links"); + + if (toggle && links) { + toggle.addEventListener("click", function () { + var open = links.classList.toggle("open"); + toggle.setAttribute("aria-expanded", String(open)); + }); + } + + document.querySelectorAll(".install-tab").forEach(function (tab) { + tab.addEventListener("click", function () { + var target = tab.getAttribute("aria-controls"); + document.querySelectorAll(".install-tab").forEach(function (item) { + item.setAttribute("aria-selected", String(item === tab)); + }); + document.querySelectorAll(".install-panel").forEach(function (panel) { + panel.hidden = panel.id !== target; + }); + }); + }); +})(); diff --git a/docs/assets/upgopher-demo.gif b/docs/assets/upgopher-demo.gif new file mode 100644 index 0000000..2ce43c2 Binary files /dev/null and b/docs/assets/upgopher-demo.gif differ diff --git a/docs/configuration.html b/docs/configuration.html new file mode 100644 index 0000000..71981ba --- /dev/null +++ b/docs/configuration.html @@ -0,0 +1,101 @@ + + + + + + Configuration reference — Upgopher + + + + + + + + + + +
+

Configuration

Upgopher is configured entirely with command-line flags. No configuration file is required.

+
+ +
+

Files and port

+
upgopher -dir ./shared -port 8080
+

-dir selects the shared directory and defaults to ./uploads. The directory is created when it does not exist. -port defaults to 9090, or 443 when -ssl is explicitly enabled without a custom port.

+ +

Access controls

+

Basic Auth

+
upgopher -user admin -pass 'choose-a-strong-password'
+

Both flags are required together. Authentication covers the registered application routes globally; Upgopher does not provide multiple accounts or per-folder roles.

+

Read-only mode

+
upgopher -dir ./public -readonly
+

Disables uploads, file and directory deletion, and directory creation through the application while retaining browsing and downloads.

+

Hidden files

+
upgopher -disable-hidden-files
+

Removes the UI control for showing hidden files and keeps them out of listings.

+ +

HTTPS

+

Self-signed certificate

+
upgopher -ssl
+

A new in-memory self-signed certificate is generated at startup. Browsers will warn because it is not signed by a trusted certificate authority.

+

Custom certificate

+
upgopher -ssl -cert /path/to/cert.pem -key /path/to/key.pem
+

Provide both certificate and private key. For public deployments, a reverse proxy that manages trusted certificates is usually easier to operate.

+ +

Limits and timeouts

+
upgopher \
+  -max-upload-size 1 \
+  -max-tabs 5 \
+  -read-header-timeout 10s \
+  -read-timeout 30m \
+  -write-timeout 30m
+

Upload size is expressed in GiB; zero means unlimited. Read and write timeout values use Go duration syntax such as 30s, 5m, or 1h. Zero disables the respective overall timeout.

+ +

Flag reference

+
+ + + + + + + + + + + + + + + + + + + +
FlagDefaultPurpose
-dir./uploadsDirectory to share.
-port9090TCP port; defaults to 443 when only -ssl changes it.
-useremptyBasic Auth username; requires -pass.
-passemptyBasic Auth password; requires -user.
-sslfalseEnable HTTPS.
-certemptyPEM certificate path for HTTPS.
-keyemptyPEM private key path for HTTPS.
-readonlyfalseDisable application upload, create, and delete operations.
-disable-hidden-filesfalseKeep hidden files out of the UI.
-max-upload-size0Maximum upload size in GiB; zero is unlimited.
-max-tabs10Maximum shared clipboard tabs.
-read-header-timeout10sMaximum time to read request headers.
-read-timeout0Maximum time to read a complete request; zero is unlimited.
-write-timeout0Maximum response write duration; zero is unlimited.
-qfalseQuiet mode.
+
+

The executable remains the source of truth. Run upgopher -h to inspect the flags in your installed version.

+
+
+
+ + + diff --git a/docs/getting-started.html b/docs/getting-started.html new file mode 100644 index 0000000..28c0bfa --- /dev/null +++ b/docs/getting-started.html @@ -0,0 +1,101 @@ + + + + + + Getting started — Upgopher + + + + + + + + + + +
+
+
+ +

Getting started

+

Install one binary, choose a directory, and open the server from a browser.

+
+
+
+ +
+

Install

+

macOS with Homebrew

+
brew install --cask wanetty/tap/upgopher
+

Windows with Scoop

+
scoop bucket add wanetty https://github.com/wanetty/scoop-bucket
+scoop install upgopher
+

Any supported OS with Go

+
go install github.com/wanetty/upgopher@latest
+

Go places the executable in $(go env GOPATH)/bin. Make sure that directory is on your PATH.

+

Release archive

+

Download the archive for Linux, macOS, or Windows from the latest GitHub release. Extract it and put the executable somewhere on your PATH.

+ +

Share your first directory

+
mkdir shared
+upgopher -dir ./shared
+

Open http://localhost:9090. Other devices on the same network can use your machine's LAN address, for example http://192.168.1.20:9090.

+
Network exposure: Upgopher listens on all interfaces. Add -user and -pass, or limit access with your network, VPN, firewall, or reverse proxy.
+
upgopher -dir ./shared -user admin -pass 'choose-a-strong-password'
+ +

Run with Docker

+
docker run --rm \
+  --name upgopher \
+  -p 9090:9090 \
+  -v "$PWD:/data" \
+  wanetty/upgopher:1.20.0
+

The image runs as a non-root user and serves /data. Ensure that the mounted directory is readable and writable by UID/GID 65532, or use -readonly with read permission only.

+
docker run -d \
+  --name upgopher \
+  --restart unless-stopped \
+  -p 9090:9090 \
+  -v "/srv/share:/data" \
+  ghcr.io/wanetty/upgopher:1.20.0 \
+  -dir /data -user admin -pass 'choose-a-strong-password'
+

Use an exact version tag for predictable deployments. Move to a newer version deliberately after reading its release notes.

+ +

Verify a downloaded archive

+

Every release includes checksums.txt. Download it next to the archive and compare the SHA-256 digest.

+

macOS or Linux

+
shasum -a 256 upgopher_1.20.0_darwin_arm64.tar.gz
+grep upgopher_1.20.0_darwin_arm64.tar.gz checksums.txt
+

Windows PowerShell

+
Get-FileHash .\upgopher_1.20.0_windows_amd64.zip -Algorithm SHA256
+Select-String upgopher_1.20.0_windows_amd64.zip .\checksums.txt
+ +

Next steps

+ +
+
+
+ + + diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..980d9c6 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,214 @@ + + + + + + Upgopher — Share files and clipboard content in one binary + + + + + + + + + + + + + + + + + +
+
+
+

Portable file sharing / v1.20.0

+

One binary.
Your files.
Any browser.

+

Share files, folders, text, and screenshots between devices. No database, external runtime, or configuration file required.

+ +
+
+ + + Upgopher browser interface demonstrating file browsing, uploads, and the shared clipboard + +
+
+ + + +
+
+
+
+ +

Small server.
Useful jobs.

+
+

Upgopher sits between a read-only development server and a full storage platform: enough interface to be useful, little enough infrastructure to start and remove in minutes.

+
+
+
+ 01 / LAN DROP +

Move files without a client

+

Open one address from a phone, laptop, VM, or lab machine and start transferring.

+
+
+ 02 / CLIPBOARD +

Bridge device clipboards

+

Share text and screenshots through protected tabs when native clipboard sync is unavailable.

+
+
+ 03 / DEVELOPMENT +

Expose a working directory

+

Hand build output, logs, or test artifacts to another device with a single command.

+
+
+ 04 / READ ONLY +

Open a download portal

+

Keep navigation and downloads available while blocking uploads and deletions.

+
+
+ 05 / HOMELAB +

Add a compact file drop

+

Run it behind a VPN, tunnel, or reverse proxy without adding an application database.

+
+
+ 06 / SUPPORT +

Collect or deliver artifacts

+

Give a temporary browser UI to people who should not need a shell or transfer tool.

+
+
+
+
+ +
+
+
+ +

Pick a package.
Keep one process.

+

Every method produces the same self-contained server. Package-manager repositories and container images are maintained by the project.

+ +
+
+
installation / choose a platform
+
+
+ + + + +
+
+
$ brew install --cask wanetty/tap/upgopher
+$ upgopher -dir ./shared
+ + + +
+
+
+ +
+
+
+
+ +

A practical UI,
already inside.

+
+

Browse folders, upload with progress, download selections as ZIP, search text files, and move clipboard content across screens.

+
+
+
+ Upgopher file manager showing files, upload controls, breadcrumbs, and file actions +
File manager / upload and browse
+
+
+ Upgopher shared clipboard with tabs, text content, and screenshot sharing +
Shared clipboard / text and images
+
+
+ Upgopher directory tree panel with nested folders +
Directory tree / fast navigation
+
+
+ Upgopher file manager adapted to a narrow mobile viewport +
Responsive UI / phone viewport
+
+
+
+
+ +
+
+
+
+ +

Choose the tool
that fits the job.

+
+

Upgopher optimizes for a useful browser interface with minimal setup. It is not trying to replace multi-user storage platforms or protocol-rich transfer servers.

+
+
+ + + + + + + + + + + +
ToolSetupStrengthChoose it when…
UpgopherOne binaryFiles + shared clipboardYou want a practical browser UI without a database or config file.
python http.serverPython runtimeRead-only servingYou only need to expose local files briefly.
DufsOne binaryWebDAV + resumable transfersTransfer protocol support matters more than clipboard sharing.
File BrowserPersistent app stateUsers + administrationYou need accounts, roles, and a managed file portal.
CopypartyPython / bundleProtocols + advanced transfersYou need resumability, indexing, deduplication, or media features.
+
+ +
+
+ +
+
+

Turn a folder into a sharing surface before the coffee cools.

+ Get started +
+
+
+ + + + diff --git a/docs/security.html b/docs/security.html new file mode 100644 index 0000000..05b3c02 --- /dev/null +++ b/docs/security.html @@ -0,0 +1,78 @@ + + + + + + Security — Upgopher + + + + + + + + + + +
+

Security

Upgopher provides useful controls, but the network and host remain part of the security boundary.

+
+ +
+

Security model

+

Upgopher shares one configured directory through HTTP. Filesystem operations validate that resolved paths stay within that directory, user paths in URLs remain encoded, and the application does not intentionally expose absolute host paths in browser-facing errors.

+

All application routes can be wrapped in one optional Basic Auth credential pair. This is suitable for small trusted deployments, not for multi-tenant authorization.

+ +

Network exposure

+
Default: the server binds to 0.0.0.0. Without Basic Auth, anyone who can reach the port can use the enabled operations.
+

For LAN use, apply host firewall rules if the network includes untrusted devices. For remote access, prefer a private VPN or a reverse proxy with trusted TLS and additional access controls. Do not assume an obscure port provides protection.

+ +

Basic authentication

+
upgopher -user admin -pass 'a-long-unique-password'
+

Both flags are required. The credential protects the application globally and is sent by the browser on requests after login. Basic Auth requires TLS when traffic crosses an untrusted network.

+

Secrets supplied as command arguments may be visible to other privileged users through process inspection. Use host-level isolation appropriate for your environment.

+ +

TLS options

+
# Local testing; clients will show a trust warning
+upgopher -ssl
+
+# Existing trusted certificate and key
+upgopher -ssl -cert /run/secrets/fullchain.pem -key /run/secrets/privkey.pem
+

The automatically generated certificate is self-signed, in memory, and recreated on restart. It encrypts traffic but does not establish a trusted identity. Public deployments should use a trusted certificate, commonly through a reverse proxy.

+ +

Read-only mode

+
upgopher -dir /srv/public -readonly
+

Read-only mode blocks mutating operations exposed by Upgopher. It does not modify filesystem permissions or prevent another process on the host from changing files. Defense in depth means also mounting or granting the directory read-only when practical.

+ +

State and retention

+

Uploaded files remain in the shared directory. Clipboard text, clipboard images, clipboard tab metadata, and custom path aliases are process memory and disappear when Upgopher restarts. Clipboard content should therefore be treated as temporary, not durable or encrypted storage.

+

Temporary ZIP files are created in the operating system temporary directory while downloads are assembled and are removed after serving.

+ +

Report a vulnerability

+

Contact @gm_eduard privately with the affected version, reproduction steps, impact, and any suggested remediation. Please do not open a public issue before coordinating disclosure.

+
+
+
+ + + diff --git a/docs/troubleshooting.html b/docs/troubleshooting.html new file mode 100644 index 0000000..b58deae --- /dev/null +++ b/docs/troubleshooting.html @@ -0,0 +1,79 @@ + + + + + + Troubleshooting — Upgopher + + + + + + + + + + +
+

Troubleshooting

Start with the process, then the local browser, then the network boundary.

+
+ +
+

Another device cannot connect

+
    +
  1. Confirm the server is running and note the port in its log.
  2. +
  3. Open http://localhost:9090 on the host itself.
  4. +
  5. Find the host's LAN address and open http://LAN-IP:9090 from the other device.
  6. +
  7. Allow inbound TCP traffic for that port in the host firewall.
  8. +
  9. Confirm both devices can route to each other; guest Wi-Fi often isolates clients.
  10. +
+

When -ssl is enabled, use https://. Its default port becomes 443 unless -port was explicitly supplied.

+ +

Uploads or ZIP downloads report permission errors

+

The process needs read access to shared files, write access for uploads and mutations, and write access to the operating system temporary directory for ZIP creation.

+

Docker bind mounts

+

The project image runs as UID/GID 65532. Grant that identity appropriate access to the host directory, or run the container in read-only mode with readable files.

+
sudo chown -R 65532:65532 /srv/share
+

Choose permissions that match your host's ownership model; do not make a sensitive directory world-writable just to bypass the error.

+ +

Large uploads stop early

+

Check -max-upload-size, reverse-proxy limits, tunnel limits, available disk space, and request timeouts. For slow large transfers, increase or disable the overall read and write timeouts:

+
upgopher -read-timeout 0 -write-timeout 0
+

Upgopher does not implement resumable uploads. An interrupted upload must be restarted.

+ +

The browser warns about the certificate

+

This is expected when using -ssl without -cert and -key. The generated certificate is self-signed. For trusted access, provide a certificate trusted by your devices or terminate TLS at a reverse proxy.

+ +

The login prompt repeats

+

Confirm both -user and -pass are present, credentials are entered exactly, and a reverse proxy is forwarding the Authorization header. Try a private browser window after changing credentials to avoid cached Basic Auth state.

+ +

Collect diagnostics

+
upgopher -h
+go version
+uname -a   # macOS or Linux
+

When opening an issue, include the Upgopher version, operating system and architecture, exact command with passwords removed, expected behavior, and sanitized logs. Never publish credentials or absolute paths containing private information.

+
+
+
+ + + diff --git a/scripts/check-community-scripts-contract.sh b/scripts/check-community-scripts-contract.sh new file mode 100755 index 0000000..30ff6b1 --- /dev/null +++ b/scripts/check-community-scripts-contract.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -eu + +dist_dir="${1:-dist}" + +for architecture in amd64 arm64; do + set -- "$dist_dir"/upgopher_*_linux_"$architecture".tar.gz + + if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then + echo "expected one Linux $architecture archive for Community Scripts" >&2 + exit 1 + fi + + archive="$1" + archive_name="${archive##*/}" + archive_root="${archive_name%.tar.gz}" + + if ! tar -tzf "$archive" | grep -Fxq "$archive_root/upgopher"; then + echo "$archive_name does not contain $archive_root/upgopher" >&2 + exit 1 + fi +done + +echo "Community Scripts release contract is compatible" diff --git a/scripts/container-smoke-test.sh b/scripts/container-smoke-test.sh new file mode 100644 index 0000000..cca5d51 --- /dev/null +++ b/scripts/container-smoke-test.sh @@ -0,0 +1,72 @@ +#!/bin/sh +set -eu + +engine="${CONTAINER_ENGINE:-podman}" +image="${1:-localhost/upgopher:adoption-test}" +name="upgopher-smoke-$$" +fixture_dir="$(mktemp -d)" +current_container="" + +cleanup() { + if [ -n "$current_container" ]; then + "$engine" stop --time 0 "$current_container" >/dev/null 2>&1 || true + fi + rm -rf "$fixture_dir" +} +trap cleanup EXIT INT TERM + +start_container() { + current_container="$name-$1" + shift + "$engine" run -d --rm --network host --name "$current_container" "$image" "$@" >/dev/null +} + +stop_container() { + "$engine" stop --time 0 "$current_container" >/dev/null + current_container="" +} + +wait_for_url() { + curl --retry 20 --retry-connrefused --retry-delay 1 --silent --show-error \ + --output /dev/null "$1" +} + +image_user="$("$engine" image inspect --format '{{.Config.User}}' "$image")" +test "$image_user" = "65532:65532" + +printf '%s\n' "Upgopher container smoke test" >"$fixture_dir/payload.txt" + +start_container files +wait_for_url http://127.0.0.1:9090/ +upload_code="$(curl --silent --output /dev/null --write-out '%{http_code}' \ + --form "file=@$fixture_dir/payload.txt" http://127.0.0.1:9090/)" +test "$upload_code" = "303" +zip_code="$(curl --silent --output "$fixture_dir/files.zip" --write-out '%{http_code}' \ + http://127.0.0.1:9090/zip)" +test "$zip_code" = "200" +test -s "$fixture_dir/files.zip" +stop_container + +start_container readonly -dir /data -readonly +wait_for_url http://127.0.0.1:9090/ +readonly_code="$(curl --silent --output /dev/null --write-out '%{http_code}' \ + --form "file=@$fixture_dir/payload.txt" http://127.0.0.1:9090/)" +test "$readonly_code" = "403" +stop_container + +start_container auth -dir /data -user demo -pass smoke-test-only +wait_for_url http://127.0.0.1:9090/ +unauthorized_code="$(curl --silent --output /dev/null --write-out '%{http_code}' \ + http://127.0.0.1:9090/)" +authorized_code="$(curl --silent --output /dev/null --write-out '%{http_code}' \ + --user demo:smoke-test-only http://127.0.0.1:9090/)" +test "$unauthorized_code" = "401" +test "$authorized_code" = "200" +stop_container + +start_container tls -dir /data -ssl -port 9443 +curl --insecure --retry 20 --retry-connrefused --retry-delay 1 --silent \ + --show-error --output /dev/null https://127.0.0.1:9443/ +stop_container + +printf '%s\n' "container smoke test passed" diff --git a/scripts/create-demo-gif.go b/scripts/create-demo-gif.go new file mode 100644 index 0000000..1e50041 --- /dev/null +++ b/scripts/create-demo-gif.go @@ -0,0 +1,88 @@ +// Command create-demo-gif builds the README animation from product screenshots. +// It uses only the Go standard library so maintainers can refresh the asset +// without installing a media toolchain. +package main + +import ( + "fmt" + "image" + "image/color/palette" + "image/draw" + "image/gif" + _ "image/jpeg" + _ "image/png" + "os" +) + +const ( + outputWidth = 960 + outputHeight = 700 +) + +func main() { + frames := []struct { + path string + delay int + }{ + {"docs/assets/file-manager.png", 220}, + {"docs/assets/directory-tree.png", 180}, + {"docs/assets/clipboard.png", 220}, + } + + animation := &gif.GIF{LoopCount: 0} + for _, frame := range frames { + source, err := decode(frame.path) + if err != nil { + fatal(err) + } + + scaled := resizeNearest(source, outputWidth, outputHeight) + paletted := image.NewPaletted(scaled.Bounds(), palette.Plan9) + draw.FloydSteinberg.Draw(paletted, paletted.Rect, scaled, image.Point{}) + animation.Image = append(animation.Image, paletted) + animation.Delay = append(animation.Delay, frame.delay) + animation.Disposal = append(animation.Disposal, gif.DisposalNone) + } + + output, err := os.Create("docs/assets/upgopher-demo.gif") + if err != nil { + fatal(err) + } + defer output.Close() + + if err := gif.EncodeAll(output, animation); err != nil { + fatal(err) + } +} + +func decode(path string) (image.Image, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + defer file.Close() + + img, _, err := image.Decode(file) + if err != nil { + return nil, fmt.Errorf("decode %s: %w", path, err) + } + return img, nil +} + +func resizeNearest(source image.Image, width, height int) *image.RGBA { + bounds := source.Bounds() + target := image.NewRGBA(image.Rect(0, 0, width, height)) + for y := 0; y < height; y++ { + sourceY := bounds.Min.Y + y*bounds.Dy()/height + for x := 0; x < width; x++ { + sourceX := bounds.Min.X + x*bounds.Dx()/width + target.Set(x, y, source.At(sourceX, sourceY)) + } + } + return target +} + +func fatal(err error) { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) +} diff --git a/scripts/enrich-scoop-manifest/main.go b/scripts/enrich-scoop-manifest/main.go new file mode 100644 index 0000000..b563845 --- /dev/null +++ b/scripts/enrich-scoop-manifest/main.go @@ -0,0 +1,58 @@ +// Command enrich-scoop-manifest adds Scoop's release discovery metadata to the +// manifest generated by GoReleaser. +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +func main() { + if len(os.Args) != 2 { + fatalf("usage: enrich-scoop-manifest ") + } + + path := os.Args[1] + data, err := os.ReadFile(path) + if err != nil { + fatalf("read manifest: %v", err) + } + + var manifest map[string]any + if err := json.Unmarshal(data, &manifest); err != nil { + fatalf("decode manifest: %v", err) + } + + manifest["checkver"] = "github" + manifest["autoupdate"] = map[string]any{ + "architecture": map[string]any{ + "32bit": map[string]string{ + "url": "https://github.com/wanetty/upgopher/releases/download/v$version/upgopher_$version_windows_386.zip", + }, + "64bit": map[string]string{ + "url": "https://github.com/wanetty/upgopher/releases/download/v$version/upgopher_$version_windows_amd64.zip", + }, + "arm64": map[string]string{ + "url": "https://github.com/wanetty/upgopher/releases/download/v$version/upgopher_$version_windows_arm64.zip", + }, + }, + "hash": map[string]string{ + "url": "$baseurl/checksums.txt", + }, + } + + output, err := json.MarshalIndent(manifest, "", " ") + if err != nil { + fatalf("encode manifest: %v", err) + } + output = append(output, '\n') + if err := os.WriteFile(path, output, 0o644); err != nil { + fatalf("write manifest: %v", err) + } +} + +func fatalf(format string, args ...any) { + _, _ = fmt.Fprintf(os.Stderr, format+"\n", args...) + os.Exit(1) +} diff --git a/upgopher.go b/upgopher.go index ceae5c5..cee54a2 100644 --- a/upgopher.go +++ b/upgopher.go @@ -29,7 +29,7 @@ var favicon embed.FS var logo embed.FS var quiet bool = false -var version = "1.19.1" +var version = "1.20.0" var showHiddenFiles bool = false var disableHiddenFiles bool = false var readOnly bool = false