Full Code of dutchcoders/transfer.sh for AI

main 6743a4cf4621 cached
38 files
226.0 KB
73.0k tokens
186 symbols
1 requests
Download .txt
Showing preview only (237K chars total). Download the full file or copy to clipboard to get everything.
Repository: dutchcoders/transfer.sh
Branch: main
Commit: 6743a4cf4621
Files: 38
Total size: 226.0 KB

Directory structure:
gitextract_9idmy_oq/

├── .bowerrc
├── .dockerignore
├── .github/
│   ├── build/
│   │   └── friendly-filenames.json
│   └── workflows/
│       ├── build-docker-images.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .golangci.yml
├── .jshintrc
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── Vagrantfile
├── cmd/
│   └── cmd.go
├── examples.md
├── extras/
│   ├── clamd
│   └── transfersh
├── flake.nix
├── go.mod
├── go.sum
├── main.go
├── manifest.json
└── server/
    ├── clamav.go
    ├── handlers.go
    ├── handlers_test.go
    ├── ip_filter.go
    ├── server.go
    ├── storage/
    │   ├── common.go
    │   ├── gdrive.go
    │   ├── local.go
    │   ├── s3.go
    │   └── storj.go
    ├── token.go
    ├── token_test.go
    ├── utils.go
    └── virustotal.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .bowerrc
================================================
{
    "directory": "transfersh-web/bower_components"
}


================================================
FILE: .dockerignore
================================================
build
pkg
dist
src
bin
*.pyc
*.egg-info
.vagrant
.git
.tmp
bower_components
node_modules
extras
build
transfersh-server/run.sh
.elasticbeanstalk
Dockerfile


================================================
FILE: .github/build/friendly-filenames.json
================================================
{
    "android-arm64": { "friendlyName": "android-arm64-v8a" },
    "darwin-amd64": { "friendlyName": "darwin-amd64" },
    "darwin-arm64": { "friendlyName": "darwin-arm64" },
    "dragonfly-amd64": { "friendlyName": "dragonfly-amd64" },
    "freebsd-386": { "friendlyName": "freebsd-386" },
    "freebsd-amd64": { "friendlyName": "freebsd-amd64" },
    "freebsd-arm64": { "friendlyName": "freebsd-arm64-v8a" },
    "freebsd-arm7": { "friendlyName": "freebsd-arm32-v7a" },
    "linux-386": { "friendlyName": "linux-386" },
    "linux-amd64": { "friendlyName": "linux-amd64" },
    "linux-arm5": { "friendlyName": "linux-arm32-v5" },
    "linux-arm64": { "friendlyName": "linux-arm64-v8a" },
    "linux-arm6": { "friendlyName": "linux-arm32-v6" },
    "linux-arm7": { "friendlyName": "linux-armv7" },
    "linux-mips64le": { "friendlyName": "linux-mips64le" },
    "linux-mips64": { "friendlyName": "linux-mips64" },
    "linux-mipslesoftfloat": { "friendlyName": "linux-mips32le-softfloat" },
    "linux-mipsle": { "friendlyName": "linux-mips32le" },
    "linux-mipssoftfloat": { "friendlyName": "linux-mips32-softfloat" },
    "linux-mips": { "friendlyName": "linux-mips32" },
    "linux-ppc64le": { "friendlyName": "linux-ppc64le" },
    "linux-ppc64": { "friendlyName": "linux-ppc64" },
    "linux-riscv64": { "friendlyName": "linux-riscv64" },
    "linux-s390x": { "friendlyName": "linux-s390x" },
    "openbsd-386": { "friendlyName": "openbsd-386" },
    "openbsd-amd64": { "friendlyName": "openbsd-amd64" },
    "openbsd-arm64": { "friendlyName": "openbsd-arm64-v8a" },
    "openbsd-arm7": { "friendlyName": "openbsd-arm32-v7a" },
    "windows-386": { "friendlyName": "windows-386" },
    "windows-amd64": { "friendlyName": "windows-amd64" },
    "windows-arm7": { "friendlyName": "windows-arm32-v7a" }
  }


================================================
FILE: .github/workflows/build-docker-images.yml
================================================
name: deploy multi-architecture Docker images for transfer.sh with buildx

on:
  schedule:
    - cron: '0 0 * * *' # everyday at midnight UTC
  pull_request:
    branches: main
  push:
    branches: main
    tags:
      - v*

jobs:
  buildx:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout
        uses: actions/checkout@v2
      -
        name: Prepare
        id: prepare
        run: |
          DOCKER_IMAGE=dutchcoders/transfer.sh
          DOCKER_PLATFORMS=linux/amd64,linux/arm/v7,linux/arm64,linux/386
          VERSION=edge

          if [[ $GITHUB_REF == refs/tags/* ]]; then
            VERSION=v${GITHUB_REF#refs/tags/v}
          fi

          if [ "${{ github.event_name }}" = "schedule" ]; then
            VERSION=nightly
          fi

          TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
          TAGS_NOROOT="--tag ${DOCKER_IMAGE}:${VERSION}-noroot"

          if [ $VERSION = edge -o $VERSION = nightly ]; then
            TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest"
            TAGS_NOROOT="$TAGS_NOROOT --tag ${DOCKER_IMAGE}:latest-noroot"
          fi

          echo ::set-output name=docker_image::${DOCKER_IMAGE}
          echo ::set-output name=version::${VERSION}
          echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
            --build-arg VERSION=${VERSION} \
            --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
            --build-arg VCS_REF=${GITHUB_SHA::8} \
            ${TAGS} .
          echo ::set-output name=buildx_args_noroot::--platform ${DOCKER_PLATFORMS} \
            --build-arg VERSION=${VERSION} \
            --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
            --build-arg VCS_REF=${GITHUB_SHA::8} \
            --build-arg RUNAS=noroot \
            ${TAGS_NOROOT} .
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v1
        with:
          platforms: all
      -
        name: Set up Docker Buildx
        id: buildx
        uses: docker/setup-buildx-action@v1
        with:
          version: latest
      -
        name: Available platforms
        run: echo ${{ steps.buildx.outputs.platforms }}
      -
        name: Docker Buildx (build)
        run: |
          docker buildx build --no-cache --pull --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}
          docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args_noroot }}
      -
        name: Docker Login
        if: success() && github.event_name != 'pull_request'
        env:
          DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
          DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
        run: |
          echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
      -
        name: Docker Buildx (push)
        if: success() && github.event_name != 'pull_request'
        run: |
          docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }}
          docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args_noroot }}
      -
        name: Docker Check Manifest
        if: always() && github.event_name != 'pull_request'
        run: |
          docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}
          docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}-noroot
      -
        name: Clear
        if: always() && github.event_name != 'pull_request'
        run: |
          rm -f ${HOME}/.docker/config.json


================================================
FILE: .github/workflows/release.yml
================================================
name: Build and Release

on:
  workflow_dispatch:
  release:
    types: [published]
jobs:
  build:
    strategy:
      matrix:
        # Include amd64 on all platforms.
        goos: [windows, freebsd, openbsd, linux, dragonfly, darwin]
        goarch: [amd64, 386]
        exclude:
          # Exclude i386 on darwin and dragonfly.
          - goarch: 386
            goos: dragonfly
          - goarch: 386
            goos: darwin
        include:
          # BEIGIN MacOS ARM64
          - goos: darwin
            goarch: arm64
          # END MacOS ARM64
          # BEGIN Linux ARM 5 6 7
          - goos: linux
            goarch: arm
            goarm: 7
          - goos: linux
            goarch: arm
            goarm: 6
          - goos: linux
            goarch: arm
            goarm: 5
          # END Linux ARM 5 6 7
          # BEGIN Android ARM 8
          - goos: android
            goarch: arm64
          # END Android ARM 8
          # Windows ARM 7
          - goos: windows
            goarch: arm
            goarm: 7
          # BEGIN Other architectures
          # BEGIN riscv64 & ARM64
          - goos: linux
            goarch: arm64
          - goos: linux
            goarch: riscv64
          # END riscv64 & ARM64
          # BEGIN MIPS
          - goos: linux
            goarch: mips64
          - goos: linux
            goarch: mips64le
          - goos: linux
            goarch: mipsle
          - goos: linux
            goarch: mips
          # END MIPS
          # BEGIN PPC
          - goos: linux
            goarch: ppc64
          - goos: linux
            goarch: ppc64le
          # END PPC
          # BEGIN FreeBSD ARM
          - goos: freebsd
            goarch: arm64
          - goos: freebsd
            goarch: arm
            goarm: 7
          # END FreeBSD ARM
          # BEGIN S390X
          - goos: linux
            goarch: s390x
          # END S390X
          # END Other architectures
          # BEGIN OPENBSD ARM
          - goos: openbsd
            goarch: arm64
          - goos: openbsd
            goarch: arm
            goarm: 7
          # END OPENBSD ARM
      fail-fast: false

    runs-on: ubuntu-latest
    env:
      GOOS: ${{ matrix.goos }}
      GOARCH: ${{ matrix.goarch }}
      GOARM: ${{ matrix.goarm }}
      CGO_ENABLED: 0
    steps:
      - name: Checkout codebase
        uses: actions/checkout@v2

      - name: Show workflow information 
        id: get_filename
        run: |
          export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM$GOMIPS\"].friendlyName" -r < .github/build/friendly-filenames.json)
          echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, GOMIPS: $GOMIPS, RELEASE_NAME: $_NAME"
          echo "::set-output name=ASSET_NAME::$_NAME"
          echo "::set-output name=GIT_TAG::${GITHUB_REF##*/}"
          echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: ^1.22

      - name: Get project dependencies
        run: go mod download
 
      - name: Build Transfersh
        run: |
          mkdir -p build_assets
          go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=${GITHUB_REF##*/} -a -s -w -extldflags '-static'" -o build_assets/transfersh-${GITHUB_REF##*/}-${ASSET_NAME}
    
      - name: Build Mips softfloat Transfersh
        if: matrix.goarch == 'mips' || matrix.goarch == 'mipsle'
        run: |
          GOMIPS=softfloat go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=${GITHUB_REF##*/} -a -s -w -extldflags '-static'" -o build_assets/transfersh-softfloat-${GITHUB_REF##*/}-${ASSET_NAME}

      - name: Rename Windows Transfersh
        if: matrix.goos == 'windows'
        run: |
          cd ./build_assets || exit 1
          mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME} transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.exe

      - name: Prepare to release
        run: |
          cp ${GITHUB_WORKSPACE}/README.md ./build_assets/README.md
          cp ${GITHUB_WORKSPACE}/LICENSE ./build_assets/LICENSE

      - name: Create Gzip archive
        shell: bash
        run: |
          pushd build_assets || exit 1
          touch -mt $(date +%Y01010000) *
          tar zcvf transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz *
          mv transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz ../
          FILE=`find . -name "transfersh-${GITHUB_REF##*/}-${ASSET_NAME}*"`
          DGST=$FILE.sha256sum
          echo `sha256sum $FILE` > $DGST
          popd || exit 1
          FILE=./transfersh-${GITHUB_REF##*/}-${ASSET_NAME}.tar.gz
          DGST=$FILE.sha256sum
          echo `sha256sum $FILE` > $DGST

      - name: Change the name
        run: |
          mv build_assets transfersh-${GITHUB_REF##*/}-${ASSET_NAME}

      - name: Upload files to Artifacts
        uses: actions/upload-artifact@v2
        with:
          name: transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}
          path: |
            ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/*

      - name: Upload binaries to release
        uses: softprops/action-gh-release@v1
        if: github.event_name == 'release'
        with:
          files: |
            ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}.tar.gz*
            ./transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}/transfersh-${{ steps.get_filename.outputs.GIT_TAG }}-${{ steps.get_filename.outputs.ASSET_NAME }}*
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}



================================================
FILE: .github/workflows/test.yml
================================================
name: test
on:
  pull_request:
    branches:
      - "*"
  push:
    branches:
      - "*"
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        go_version:
          - '1.22'
          - '1.23'
          - '1.24'
          - tip
    name: Test with ${{ matrix.go_version }}
    steps:
      - uses: actions/checkout@v2
      - name: Install Go ${{ matrix.go_version }}
        if: ${{ matrix.go_version != 'tip' }}
        uses: actions/setup-go@master
        with:
          go-version: ${{ matrix.go_version }}
          check-latest: true
      - name: Install Go ${{ matrix.go_version }}
        if: ${{ matrix.go_version == 'tip' }}
        run: |
          go install golang.org/dl/gotip@latest
          `go env GOPATH`/bin/gotip download
      - name: Vet and test no tip
        if: ${{ matrix.go_version != 'tip' }}
        run: |
          go version
          go vet ./...
          go test ./...
      - name: Vet and test gotip
        if: ${{ matrix.go_version == 'tip' }}
        run: |
          `go env GOPATH`/bin/gotip version
          `go env GOPATH`/bin/gotip vet ./...
          `go env GOPATH`/bin/gotip test ./...
  golangci:
    name: Linting
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-go@master
        with:
          go-version: '1.24'
          check-latest: true
      - name: golangci-lint
        uses: golangci/golangci-lint-action@v2
        with:
          version: latest
          skip-go-installation: true
          args: "--config .golangci.yml"


================================================
FILE: .gitignore
================================================
build/
pkg/
dist/
src/
bin/
*.pyc
*.egg-info/
.idea/

.tmp
.vagrant

bower_components/
node_modules/

transfersh-server/run.sh
.elasticbeanstalk/

# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml

!.github/build/


================================================
FILE: .golangci.yml
================================================
run:
  deadline: 10m
  issues-exit-code: 1
  tests: true

output:
  format: colored-line-number
  print-issued-lines: true
  print-linter-name: true

linters:
  disable:
    - deadcode
    - unused

issues:
  max-issues-per-linter: 0
  max-same-issues: 0
  new: false
  exclude-use-default: false


================================================
FILE: .jshintrc
================================================
{
    "node": true,
    "browser": true,
    "esnext": true,
    "bitwise": true,
    "camelcase": true,
    "curly": true,
    "eqeqeq": true,
    "immed": true,
    "indent": 2,
    "latedef": true,
    "newcap": true,
    "noarg": true,
    "quotmark": "single",
    "regexp": true,
    "undef": true,
    "unused": true,
    "strict": true,
    "trailing": true,
    "smarttabs": true,
    "jquery": true,
    "white": true
}


================================================
FILE: CODE_OF_CONDUCT.md
================================================

# Contributor Code of Conduct

As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.

We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct
* Use of harsh language

Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.

This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.

This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.2.0, available at https://www.contributor-covenant.org/version/1/2/0/code-of-conduct.html



================================================
FILE: Dockerfile
================================================
# Default to Go 1.24
ARG GO_VERSION=1.24
FROM golang:${GO_VERSION}-alpine as build

# Necessary to run 'go get' and to compile the linked binary
RUN apk add git musl-dev mailcap

WORKDIR /go/src/github.com/dutchcoders/transfer.sh

COPY go.mod go.sum ./

RUN go mod download

COPY . .

# build & install server
RUN CGO_ENABLED=0 go build -tags netgo -ldflags "-X github.com/dutchcoders/transfer.sh/cmd.Version=$(git describe --tags) -a -s -w -extldflags '-static'" -o /go/bin/transfersh

ARG PUID=5000 \
    PGID=5000 \
    RUNAS

RUN mkdir -p /tmp/useradd /tmp/empty && \
    if [ ! -z "$RUNAS" ]; then \
    echo "${RUNAS}:x:${PUID}:${PGID}::/nonexistent:/sbin/nologin" >> /tmp/useradd/passwd && \
    echo "${RUNAS}:!:::::::" >> /tmp/useradd/shadow && \
    echo "${RUNAS}:x:${PGID}:" >> /tmp/useradd/group && \
    echo "${RUNAS}:!::" >> /tmp/useradd/groupshadow; else touch /tmp/useradd/unused; fi

FROM scratch AS final
LABEL maintainer="Andrea Spacca <andrea.spacca@gmail.com>"
ARG RUNAS

COPY --from=build /etc/mime.types /etc/mime.types
COPY --from=build /tmp/empty /tmp
COPY --from=build /tmp/useradd/* /etc/
COPY --from=build --chown=${RUNAS}  /go/bin/transfersh /go/bin/transfersh
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt

USER ${RUNAS}

ENTRYPOINT ["/go/bin/transfersh", "--listener", ":8080"]

EXPOSE 8080


================================================
FILE: LICENSE
================================================
The MIT License (MIT)

Copyright (c) 2014-2018 DutchCoders [https://github.com/dutchcoders/]
Copyright (c) 2018-2020 Andrea Spacca.
Copyright (c) 2020- Andrea Spacca and Stefan Benten.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


================================================
FILE: Makefile
================================================
.PHONY: lint

lint:
	golangci-lint run --out-format=github-actions --config .golangci.yml 



================================================
FILE: README.md
================================================
# transfer.sh [![Go Report Card](https://goreportcard.com/badge/github.com/dutchcoders/transfer.sh)](https://goreportcard.com/report/github.com/dutchcoders/transfer.sh) [![Docker pulls](https://img.shields.io/docker/pulls/dutchcoders/transfer.sh.svg)](https://hub.docker.com/r/dutchcoders/transfer.sh/) [![Build Status](https://github.com/dutchcoders/transfer.sh/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/dutchcoders/transfer.sh/actions/workflows/test.yml?query=branch%3Amain)

Easy and fast file sharing from the command-line. This code contains the server with everything you need to create your own instance.

Transfer.sh currently supports the s3 (Amazon S3), gdrive (Google Drive), storj (Storj) providers, and local file system (local).

<br />

---

<br />

## Disclaimer

@stefanbenten happens to be a maintainer of this repository _and_ the person who host a well known public installation of the software in the repo.

The two are anyway unrelated, and the repo is not the place to direct requests and issues for any of the pubblic installation.

No third-party public installation of the software in the repo will be advertised or mentioned in the repo itself, for security reasons.

The official position of me, @aspacca, as maintainer of the repo, is that if you want to use the software you should host your own installation.

<br />

---

<br />

## Usage

This section outlines how to use transfer.sh

<br />

### Upload

```bash
$ curl -v --upload-file ./hello.txt https://transfer.sh/hello.txt
```

<br />

### Encrypt & Upload

```bash
$ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt
```

<br />

### Download & Decrypt

```bash
$ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt
```

<br />

### Upload to Virustotal

```bash
$ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal
```

<br />

### Deleting

```bash
$ curl -X DELETE <X-Url-Delete Response Header URL>
```

<br />

---

<br />

## Request Headers

This section explains how to handle request headers with curl:

<br />

### Max-Downloads

```bash
$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Downloads: 1" # Limit the number of downloads
```

<br />

### Max-Days

```bash
$ curl --upload-file ./hello.txt https://transfer.sh/hello.txt -H "Max-Days: 1" # Set the number of days before deletion
```

<br />

### X-Encrypt-Password

#### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk
```bash
$ curl --upload-file ./hello.txt https://your-transfersh-instance.tld/hello.txt -H "X-Encrypt-Password: test" # Encrypt the content server side with AES256 using "test" as password
```

<br />

### X-Decrypt-Password
#### Beware, use this feature only on your self-hosted server: trusting a third-party service for server side encryption is at your own risk

```bash
$ curl https://your-transfersh-instance.tld/BAYh0/hello.txt -H "X-Decrypt-Password: test" # Decrypt the content server side with AES256 using "test" as password
```

<br />

---

<br />

## Response Headers

This section explains how to handle response headers:

<br />

### X-Url-Delete

The URL used to request the deletion of a file and returned as a response header:

```bash
curl -sD - --upload-file ./hello.txt https://transfer.sh/hello.txt | grep -i -E 'transfer\.sh|x-url-delete'
x-url-delete: https://transfer.sh/hello.txt/BAYh0/hello.txt/PDw0NHPcqU
https://transfer.sh/hello.txt/BAYh0/hello.txt
```

<br />

---

<br />

## Examples

See good usage examples on [examples.md](examples.md)

<br />

## Link aliases

Create direct download link:

https://transfer.sh/1lDau/test.txt --> https://transfer.sh/get/1lDau/test.txt

Inline file:

https://transfer.sh/1lDau/test.txt --> https://transfer.sh/inline/1lDau/test.txt

<br />

---

<br />

## Usage

Parameter | Description                                                                             | Value                         | Env                         
--- |-----------------------------------------------------------------------------------------------|-------------------------------|-------------------------------|
listener | port to use for http (:80)                                                               |                               | LISTENER                      |
profile-listener | port to use for profiler (:6060)                                                 |                               | PROFILE_LISTENER              |
force-https | redirect to https                                                                     | false                         | FORCE_HTTPS                   |
tls-listener | port to use for https (:443)                                                         |                               | TLS_LISTENER                  |
tls-listener-only | flag to enable tls listener only                                                |                               | TLS_LISTENER_ONLY             |
tls-cert-file | path to tls certificate                                                             |                               | TLS_CERT_FILE                 |
tls-private-key | path to tls private key                                                           |                               | TLS_PRIVATE_KEY               |
http-auth-user | user for basic http auth on upload                                                 |                               | HTTP_AUTH_USER                |
http-auth-pass | pass for basic http auth on upload                                                 |                               | HTTP_AUTH_PASS                |
http-auth-htpasswd | htpasswd file path for basic http auth on upload                               |                               | HTTP_AUTH_HTPASSWD            |
http-auth-ip-whitelist | comma separated list of allowed ips to upload without auth challenge       |                               | HTTP_AUTH_IP_WHITELIST        |
virustotal-key | VirusTotal API key                                                                 |                               | VIRUSTOTAL_KEY                |
ip-whitelist | comma separated list of ips allowed to connect to the service                        |                               | IP_WHITELIST                  |
ip-blacklist | comma separated list of ips not allowed to connect to the service                    |                               | IP_BLACKLIST                  |
temp-path | path to temp folder                                                                     | system temp                   | TEMP_PATH                     |
web-path | path to static web files (for development or custom front end)                           |                               | WEB_PATH                      |
proxy-path | path prefix when service is run behind a proxy (a `/` prefix will be trimmed)          |                               | PROXY_PATH                    |
proxy-port | port of the proxy when the service is run behind a proxy                               |                               | PROXY_PORT                    |
email-contact | email contact for the front end                                                     |                               | EMAIL_CONTACT                 |
ga-key | google analytics key for the front end                                                     |                               | GA_KEY                        |
provider | which storage provider to use                                                            | (s3, storj, gdrive or local)  |                               |
uservoice-key | user voice key for the front end                                                    |                               | USERVOICE_KEY                 |
aws-access-key | aws access key                                                                     |                               | AWS_ACCESS_KEY                |
aws-secret-key | aws access key                                                                     |                               | AWS_SECRET_KEY                |
bucket | aws bucket                                                                                 |                               | BUCKET                        |
s3-endpoint | Custom S3 endpoint.                                                                   |                               | S3_ENDPOINT                   |
s3-region | region of the s3 bucket                                                                 | eu-west-1                     | S3_REGION                     |
s3-no-multipart | disables s3 multipart upload                                                      | false                         | S3_NO_MULTIPART               |
s3-path-style | Forces path style URLs, required for Minio.                                         | false                         | S3_PATH_STYLE                 |
storj-access | Access for the project                                                               |                               | STORJ_ACCESS                  |
storj-bucket | Bucket to use within the project                                                     |                               | STORJ_BUCKET                  |
basedir | path storage for local/gdrive provider                                                    |                               | BASEDIR                       |
gdrive-client-json-filepath | path to oauth client json config for gdrive provider                  |                               | GDRIVE_CLIENT_JSON_FILEPATH   |
gdrive-local-config-path | path to store local transfer.sh config cache for gdrive provider         |                               | GDRIVE_LOCAL_CONFIG_PATH      |
gdrive-chunk-size | chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB) |                         | GDRIVE_CHUNK_SIZE             |
lets-encrypt-hosts | hosts to use for lets encrypt certificates (comma separated)                   |                               | HOSTS                         |
log | path to log file                                                                              |                               | LOG                           |
cors-domains | comma separated list of domains for CORS, setting it enable CORS                     |                               | CORS_DOMAINS                  |
clamav-host | host for clamav feature                                                               |                               | CLAMAV_HOST                   |
perform-clamav-prescan | prescan every upload using clamav (clamav-host must be local clamd unix socket)    |                       | PERFORM_CLAMAV_PRESCAN        |
rate-limit | request per minute                                                                     |                               | RATE_LIMIT                    |
max-upload-size | max upload size in kilobytes                                                      |                               | MAX_UPLOAD_SIZE               |
purge-days | number of days after the uploads are purged automatically                              |                               | PURGE_DAYS                    |   
purge-interval | interval (hours) to run automatic purge for (excluding S3 and Storj)               |                               | PURGE_INTERVAL                |   
random-token-length | length of random token for upload path (double the size for delete path)      | 6                             | RANDOM_TOKEN_LENGTH           |   

If you want to use TLS using lets encrypt certificates, set lets-encrypt-hosts to your domain, set tls-listener to :443 and enable force-https.

If you want to use TLS using your own certificates, set tls-listener to :443, force-https, tls-cert-file and tls-private-key.

<br />

---

<br />

## Development

Switched to GO111MODULE

```bash
go run main.go --provider=local --listener :8080 --temp-path=/tmp/ --basedir=/tmp/
```

<br />

---

<br />

## Build

```bash
$ git clone git@github.com:dutchcoders/transfer.sh.git
$ cd transfer.sh
$ go build -o transfersh main.go
```

<br />

---

<br />

## Docker

For easy deployment, we've created an official Docker container. There are two variants, differing only by which user runs the process.

The default one will run as `root`:

> [!WARNING]
> It is discouraged to use `latest` tag for WatchTower or similar tools. The `latest` tag can reference unreleased developer, test builds, and patch releases for older versions. Use an actual version tag until transfer.sh supports major or minor version tags.

```bash
docker run --publish 8080:8080 dutchcoders/transfer.sh:latest --provider local --basedir /tmp/
```

<br />

### No root

The `-noroot` tags indicate image builds that run with least priviledge to reduce the attack surface might an application get compromised.

> [!NOTE]
> Using `-noroot` is **recommended**

<br />

The one tagged with the suffix `-noroot` will use `5000` as both UID and GID:

```bash
docker run --publish 8080:8080 dutchcoders/transfer.sh:latest-noroot --provider local --basedir /tmp/
```

<br />

> [!NOTE]
> Development history details at:
> - https://github.com/dutchcoders/transfer.sh/pull/418

<br />

### Tags

Name | Usage
--|--
latest| Latest CI build, can be nightly, at commit, at tag, etc.
latest-noroot| Latest CI build, can be nightly, at commit, at tag, etc. using [no root]
nightly| Scheduled CI build every midnight UTC
nightly-noroot| Scheduled CI build every midnight UTC using [no root]
edge| Latest CI build after every commit on `main`
edge-noroot| Latest CI build after every commit on `main` using [no root]
v`x.y.z`| CI build after tagging a release
v`x.y.z`-noroot| CI build after tagging a release using [no root]

<br />

### Building the Container

You can also build the container yourself. This allows you to choose which UID/GID will be used, e.g. when using NFS mounts:

```bash
# Build arguments:
# * RUNAS: If empty, the container will run as root.
#          Set this to anything to enable UID/GID selection.
# * PUID:  UID of the process. Needs RUNAS != "". Defaults to 5000.
# * PGID:  GID of the process. Needs RUNAS != "". Defaults to 5000.

docker build -t transfer.sh-noroot --build-arg RUNAS=doesntmatter --build-arg PUID=1337 --build-arg PGID=1338 .
```

<br />

---

<br />

## S3 Usage

For the usage with a AWS S3 Bucket, you just need to specify the following options:
- provider `--provider s3`
- aws-access-key _(either via flag or environment variable `AWS_ACCESS_KEY`)_
- aws-secret-key _(either via flag or environment variable `AWS_SECRET_KEY`)_
- bucket _(either via flag or environment variable `BUCKET`)_
- s3-region _(either via flag or environment variable `S3_REGION`)_

If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.

<br />

### Custom S3 providers

To use a custom non-AWS S3 provider, you need to specify the endpoint as defined from your cloud provider.

<br />

---

<br />

## Storj Network Provider

To use the Storj Network as a storage provider you need to specify the following flags:
- provider `--provider storj`
- storj-access _(either via flag or environment variable STORJ_ACCESS)_
- storj-bucket _(either via flag or environment variable STORJ_BUCKET)_

<br />

### Creating Bucket and Scope

You need to create an access grant (or copy it from the uplink configuration) and a bucket in preparation.

To get started, log in to your account and go to the Access Grant Menu and start the Wizard on the upper right.

Enter your access grant name of choice, hit *Next* and restrict it as necessary/preferred.
Afterwards continue either in CLI or within the Browser. Next, you'll be asked for a Passphrase used as Encryption Key.
**Make sure to save it in a safe place. Without it, you will lose the ability to decrypt your files!**

Afterwards, you can copy the access grant and then start the startup of the transfer.sh endpoint. 
It is recommended to provide both the access grant and the bucket name as ENV Variables for enhanced security.

Example:

```
export STORJ_BUCKET=<BUCKET NAME>
export STORJ_ACCESS=<ACCESS GRANT>
transfer.sh --provider storj
```

<br />

---

<br />

## Google Drive Usage

For the usage with Google drive, you need to specify the following options:
- provider
- gdrive-client-json-filepath
- gdrive-local-config-path
- basedir

<br />

### Creating Gdrive Client Json

You need to create an OAuth Client id from console.cloud.google.com, download the file, and place it into a safe directory.

<br />

### Usage example

```go run main.go --provider gdrive --basedir /tmp/ --gdrive-client-json-filepath /[credential_dir] --gdrive-local-config-path [directory_to_save_config] ```

<br />

---

<br />

## Shell functions

### Bash, ash and zsh (multiple files uploaded as zip archive)
##### Add this to .bashrc or .zshrc or its equivalent
```bash
transfer() (if [ $# -eq 0 ]; then printf "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>\n">&2; return 1; fi; file_name=$(basename "$1"); if [ -t 0 ]; then file="$1"; if [ ! -e "$file" ]; then echo "$file: No such file or directory">&2; return 1; fi; if [ -d "$file" ]; then cd "$file" || return 1; file_name="$file_name.zip"; set -- zip -r -q - .; else set -- cat "$file"; fi; else set -- cat; fi; url=$("$@" | curl --silent --show-error --progress-bar --upload-file "-" "https://transfer.sh/$file_name"); echo "$url"; )
```

<br />

#### Now you can use transfer function
```
$ transfer hello.txt
```

<br />

### Bash and zsh (with delete url, delete token output and prompt before uploading)
##### Add this to .bashrc or .zshrc or its equivalent

<details><summary>Expand</summary><p>

```bash
transfer()
{
    local file
    declare -a file_array
    file_array=("${@}")

    if [[ "${file_array[@]}" == "" || "${1}" == "--help" || "${1}" == "-h" ]]
    then
        echo "${0} - Upload arbitrary files to \"transfer.sh\"."
        echo ""
        echo "Usage: ${0} [options] [<file>]..."
        echo ""
        echo "OPTIONS:"
        echo "  -h, --help"
        echo "      show this message"
        echo ""
        echo "EXAMPLES:"
        echo "  Upload a single file from the current working directory:"
        echo "      ${0} \"image.img\""
        echo ""
        echo "  Upload multiple files from the current working directory:"
        echo "      ${0} \"image.img\" \"image2.img\""
        echo ""
        echo "  Upload a file from a different directory:"
        echo "      ${0} \"/tmp/some_file\""
        echo ""
        echo "  Upload all files from the current working directory. Be aware of the webserver's rate limiting!:"
        echo "      ${0} *"
        echo ""
        echo "  Upload a single file from the current working directory and filter out the delete token and download link:"
        echo "      ${0} \"image.img\" | awk --field-separator=\": \" '/Delete token:/ { print \$2 } /Download link:/ { print \$2 }'"
        echo ""
        echo "  Show help text from \"transfer.sh\":"
        echo "      curl --request GET \"https://transfer.sh\""
        return 0
    else
        for file in "${file_array[@]}"
        do
            if [[ ! -f "${file}" ]]
            then
                echo -e "\e[01;31m'${file}' could not be found or is not a file.\e[0m" >&2
                return 1
            fi
        done
        unset file
    fi

    local upload_files
    local curl_output
    local awk_output

    du -c -k -L "${file_array[@]}" >&2
    # be compatible with "bash"
    if [[ "${ZSH_NAME}" == "zsh" ]]
    then
        read $'upload_files?\e[01;31mDo you really want to upload the above files ('"${#file_array[@]}"$') to "transfer.sh"? (Y/n): \e[0m'
    elif [[ "${BASH}" == *"bash"* ]]
    then
        read -p $'\e[01;31mDo you really want to upload the above files ('"${#file_array[@]}"$') to "transfer.sh"? (Y/n): \e[0m' upload_files
    fi

    case "${upload_files:-y}" in
        "y"|"Y")
            # for the sake of the progress bar, execute "curl" for each file.
            # the parameters "--include" and "--form" will suppress the progress bar.
            for file in "${file_array[@]}"
            do
                # show delete link and filter out the delete token from the response header after upload.
                # it is important to save "curl's" "stdout" via a subshell to a variable or redirect it to another command,
                # which just redirects to "stdout" in order to have a sane output afterwards.
                # the progress bar is redirected to "stderr" and is only displayed,
                # if "stdout" is redirected to something; e.g. ">/dev/null", "tee /dev/null" or "| <some_command>".
                # the response header is redirected to "stdout", so redirecting "stdout" to "/dev/null" does not make any sense.
                # redirecting "curl's" "stderr" to "stdout" ("2>&1") will suppress the progress bar.
                curl_output=$(curl --request PUT --progress-bar --dump-header - --upload-file "${file}" "https://transfer.sh/")
                awk_output=$(awk \
                    'gsub("\r", "", $0) && tolower($1) ~ /x-url-delete/ \
                    {
                        delete_link=$2;
                        print "Delete command: curl --request DELETE " "\""delete_link"\"";

                        gsub(".*/", "", delete_link);
                        delete_token=delete_link;
                        print "Delete token: " delete_token;
                    }

                    END{
                        print "Download link: " $0;
                    }' <<< "${curl_output}")

                # return the results via "stdout", "awk" does not do this for some reason.
                echo -e "${awk_output}\n"

                # avoid rate limiting as much as possible; nginx: too many requests.
                if (( ${#file_array[@]} > 4 ))
                then
                    sleep 5
                fi
            done
            ;;

        "n"|"N")
            return 1
            ;;

        *)
            echo -e "\e[01;31mWrong input: '${upload_files}'.\e[0m" >&2
            return 1
    esac
}
```

</p></details>

#### Sample output
```bash
$ ls -lh
total 20M
-rw-r--r-- 1 <some_username> <some_username> 10M Apr  4 21:08 image.img
-rw-r--r-- 1 <some_username> <some_username> 10M Apr  4 21:08 image2.img
$ transfer image*
10240K  image2.img
10240K  image.img
20480K  total
Do you really want to upload the above files (2) to "transfer.sh"? (Y/n):
######################################################################################################################################################################################################################################## 100.0%
Delete command: curl --request DELETE "https://transfer.sh/wJw9pz/image2.img/mSctGx7pYCId"
Delete token: mSctGx7pYCId
Download link: https://transfer.sh/wJw9pz/image2.img

######################################################################################################################################################################################################################################## 100.0%
Delete command: curl --request DELETE "https://transfer.sh/ljJc5I/image.img/nw7qaoiKUwCU"
Delete token: nw7qaoiKUwCU
Download link: https://transfer.sh/ljJc5I/image.img

$ transfer "image.img" | awk --field-separator=": " '/Delete token:/ { print $2 } /Download link:/ { print $2 }'
10240K  image.img
10240K  total
Do you really want to upload the above files (1) to "transfer.sh"? (Y/n):
######################################################################################################################################################################################################################################## 100.0%
tauN5dE3fWJe
https://transfer.sh/MYkuqn/image.img
```

<br />

---

<br />

## Contributions

Contributions are welcome.

<br />

---

<br />

## Creators

**Remco Verhoef**
- <https://twitter.com/remco_verhoef>
- <https://twitter.com/dutchcoders>

**Uvis Grinfelds**

<br />

---

<br />

## Maintainers

- **Andrea Spacca**
- **Stefan Benten**

<br />

---

<br />

## Copyright and License

Code and documentation copyright 2011-2018 Remco Verhoef.
Code and documentation copyright 2018-2020 Andrea Spacca.
Code and documentation copyright 2020- Andrea Spacca and Stefan Benten.

Code released under [the MIT license](LICENSE).


================================================
FILE: Vagrantfile
================================================
# -*- mode: ruby -*-
# vi: set ft=ruby :

# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    # Every Vagrant virtual environment requires a box to build off of.
    config.vm.box = "puphpet/ubuntu1404-x64"
    config.vm.provider "vmware_fusion" do |v|
        v.gui = true
    end
end


================================================
FILE: cmd/cmd.go
================================================
package cmd

import (
	"errors"
	"fmt"
	"log"
	"os"
	"strings"

	"github.com/dutchcoders/transfer.sh/server/storage"

	"github.com/dutchcoders/transfer.sh/server"
	"github.com/fatih/color"
	"github.com/urfave/cli/v2"
	"google.golang.org/api/googleapi"
)

// Version is inject at build time
var Version = "0.0.0"
var helpTemplate = `NAME:
{{.Name}} - {{.Usage}}

DESCRIPTION:
{{.Description}}

USAGE:
{{.Name}} {{if .Flags}}[flags] {{end}}command{{if .Flags}}{{end}} [arguments...]

COMMANDS:
{{range .Commands}}{{join .Names ", "}}{{ "\t" }}{{.Usage}}
{{end}}{{if .Flags}}
FLAGS:
{{range .Flags}}{{.}}
{{end}}{{end}}
VERSION:
` + Version +
	`{{ "\n"}}`

var globalFlags = []cli.Flag{
	&cli.StringFlag{
		Name:    "listener",
		Usage:   "127.0.0.1:8080",
		Value:   "127.0.0.1:8080",
		EnvVars: []string{"LISTENER"},
	},
	// redirect to https?
	// hostnames
	&cli.StringFlag{
		Name:    "profile-listener",
		Usage:   "127.0.0.1:6060",
		Value:   "",
		EnvVars: []string{"PROFILE_LISTENER"},
	},
	&cli.BoolFlag{
		Name:    "force-https",
		Usage:   "",
		EnvVars: []string{"FORCE_HTTPS"},
	},
	&cli.StringFlag{
		Name:    "tls-listener",
		Usage:   "127.0.0.1:8443",
		Value:   "",
		EnvVars: []string{"TLS_LISTENER"},
	},
	&cli.BoolFlag{
		Name:    "tls-listener-only",
		Usage:   "",
		EnvVars: []string{"TLS_LISTENER_ONLY"},
	},
	&cli.StringFlag{
		Name:    "tls-cert-file",
		Value:   "",
		EnvVars: []string{"TLS_CERT_FILE"},
	},
	&cli.StringFlag{
		Name:    "tls-private-key",
		Value:   "",
		EnvVars: []string{"TLS_PRIVATE_KEY"},
	},
	&cli.StringFlag{
		Name:    "temp-path",
		Usage:   "path to temp files",
		Value:   os.TempDir(),
		EnvVars: []string{"TEMP_PATH"},
	},
	&cli.StringFlag{
		Name:    "web-path",
		Usage:   "path to static web files",
		Value:   "",
		EnvVars: []string{"WEB_PATH"},
	},
	&cli.StringFlag{
		Name:    "proxy-path",
		Usage:   "path prefix when service is run behind a proxy",
		Value:   "",
		EnvVars: []string{"PROXY_PATH"},
	},
	&cli.StringFlag{
		Name:    "proxy-port",
		Usage:   "port of the proxy when the service is run behind a proxy",
		Value:   "",
		EnvVars: []string{"PROXY_PORT"},
	},
	&cli.StringFlag{
		Name:    "email-contact",
		Usage:   "email address to link in Contact Us (front end)",
		Value:   "",
		EnvVars: []string{"EMAIL_CONTACT"},
	},
	&cli.StringFlag{
		Name:    "ga-key",
		Usage:   "key for google analytics (front end)",
		Value:   "",
		EnvVars: []string{"GA_KEY"},
	},
	&cli.StringFlag{
		Name:    "uservoice-key",
		Usage:   "key for user voice (front end)",
		Value:   "",
		EnvVars: []string{"USERVOICE_KEY"},
	},
	&cli.StringFlag{
		Name:    "provider",
		Usage:   "s3|gdrive|local",
		Value:   "",
		EnvVars: []string{"PROVIDER"},
	},
	&cli.StringFlag{
		Name:    "s3-endpoint",
		Usage:   "",
		Value:   "",
		EnvVars: []string{"S3_ENDPOINT"},
	},
	&cli.StringFlag{
		Name:    "s3-region",
		Usage:   "",
		Value:   "eu-west-1",
		EnvVars: []string{"S3_REGION"},
	},
	&cli.StringFlag{
		Name:    "aws-access-key",
		Usage:   "",
		Value:   "",
		EnvVars: []string{"AWS_ACCESS_KEY"},
	},
	&cli.StringFlag{
		Name:    "aws-secret-key",
		Usage:   "",
		Value:   "",
		EnvVars: []string{"AWS_SECRET_KEY"},
	},
	&cli.StringFlag{
		Name:    "bucket",
		Usage:   "",
		Value:   "",
		EnvVars: []string{"BUCKET"},
	},
	&cli.BoolFlag{
		Name:    "s3-no-multipart",
		Usage:   "Disables S3 Multipart Puts",
		EnvVars: []string{"S3_NO_MULTIPART"},
	},
	&cli.BoolFlag{
		Name:    "s3-path-style",
		Usage:   "Forces path style URLs, required for Minio.",
		EnvVars: []string{"S3_PATH_STYLE"},
	},
	&cli.StringFlag{
		Name:    "gdrive-client-json-filepath",
		Usage:   "",
		Value:   "",
		EnvVars: []string{"GDRIVE_CLIENT_JSON_FILEPATH"},
	},
	&cli.StringFlag{
		Name:    "gdrive-local-config-path",
		Usage:   "",
		Value:   "",
		EnvVars: []string{"GDRIVE_LOCAL_CONFIG_PATH"},
	},
	&cli.IntFlag{
		Name:    "gdrive-chunk-size",
		Usage:   "",
		Value:   googleapi.DefaultUploadChunkSize / 1024 / 1024,
		EnvVars: []string{"GDRIVE_CHUNK_SIZE"},
	},
	&cli.StringFlag{
		Name:    "storj-access",
		Usage:   "Access for the project",
		Value:   "",
		EnvVars: []string{"STORJ_ACCESS"},
	},
	&cli.StringFlag{
		Name:    "storj-bucket",
		Usage:   "Bucket to use within the project",
		Value:   "",
		EnvVars: []string{"STORJ_BUCKET"},
	},
	&cli.IntFlag{
		Name:    "rate-limit",
		Usage:   "requests per minute",
		Value:   0,
		EnvVars: []string{"RATE_LIMIT"},
	},
	&cli.IntFlag{
		Name:    "purge-days",
		Usage:   "number of days after uploads are purged automatically",
		Value:   0,
		EnvVars: []string{"PURGE_DAYS"},
	},
	&cli.IntFlag{
		Name:    "purge-interval",
		Usage:   "interval in hours to run the automatic purge for",
		Value:   0,
		EnvVars: []string{"PURGE_INTERVAL"},
	},
	&cli.Int64Flag{
		Name:    "max-upload-size",
		Usage:   "max limit for upload, in kilobytes",
		Value:   0,
		EnvVars: []string{"MAX_UPLOAD_SIZE"},
	},
	&cli.StringFlag{
		Name:    "lets-encrypt-hosts",
		Usage:   "host1, host2",
		Value:   "",
		EnvVars: []string{"HOSTS"},
	},
	&cli.StringFlag{
		Name:    "log",
		Usage:   "/var/log/transfersh.log",
		Value:   "",
		EnvVars: []string{"LOG"},
	},
	&cli.StringFlag{
		Name:    "basedir",
		Usage:   "path to storage",
		Value:   "",
		EnvVars: []string{"BASEDIR"},
	},
	&cli.StringFlag{
		Name:    "clamav-host",
		Usage:   "clamav-host",
		Value:   "",
		EnvVars: []string{"CLAMAV_HOST"},
	},
	&cli.BoolFlag{
		Name:    "perform-clamav-prescan",
		Usage:   "perform-clamav-prescan",
		EnvVars: []string{"PERFORM_CLAMAV_PRESCAN"},
	},
	&cli.StringFlag{
		Name:    "virustotal-key",
		Usage:   "virustotal-key",
		Value:   "",
		EnvVars: []string{"VIRUSTOTAL_KEY"},
	},
	&cli.BoolFlag{
		Name:    "profiler",
		Usage:   "enable profiling",
		EnvVars: []string{"PROFILER"},
	},
	&cli.StringFlag{
		Name:    "http-auth-user",
		Usage:   "user for http basic auth",
		Value:   "",
		EnvVars: []string{"HTTP_AUTH_USER"},
	},
	&cli.StringFlag{
		Name:    "http-auth-pass",
		Usage:   "pass for http basic auth",
		Value:   "",
		EnvVars: []string{"HTTP_AUTH_PASS"},
	},
	&cli.StringFlag{
		Name:    "http-auth-htpasswd",
		Usage:   "htpasswd file http basic auth",
		Value:   "",
		EnvVars: []string{"HTTP_AUTH_HTPASSWD"},
	},
	&cli.StringFlag{
		Name:    "http-auth-ip-whitelist",
		Usage:   "comma separated list of ips allowed to upload without being challenged an http auth",
		Value:   "",
		EnvVars: []string{"HTTP_AUTH_IP_WHITELIST"},
	},
	&cli.StringFlag{
		Name:    "ip-whitelist",
		Usage:   "comma separated list of ips allowed to connect to the service",
		Value:   "",
		EnvVars: []string{"IP_WHITELIST"},
	},
	&cli.StringFlag{
		Name:    "ip-blacklist",
		Usage:   "comma separated list of ips not allowed to connect to the service",
		Value:   "",
		EnvVars: []string{"IP_BLACKLIST"},
	},
	&cli.StringFlag{
		Name:    "cors-domains",
		Usage:   "comma separated list of domains allowed for CORS requests",
		Value:   "",
		EnvVars: []string{"CORS_DOMAINS"},
	},
	&cli.IntFlag{
		Name:    "random-token-length",
		Usage:   "",
		Value:   10,
		EnvVars: []string{"RANDOM_TOKEN_LENGTH"},
	},
}

// Cmd wraps cli.app
type Cmd struct {
	*cli.App
}

func versionCommand(_ *cli.Context) error {
	fmt.Println(color.YellowString("transfer.sh %s: Easy file sharing from the command line", Version))
	return nil
}

// New is the factory for transfer.sh
func New() *Cmd {
	logger := log.New(os.Stdout, "[transfer.sh]", log.LstdFlags)

	app := cli.NewApp()
	app.Name = "transfer.sh"
	app.Authors = []*cli.Author{}
	app.Usage = "transfer.sh"
	app.Description = `Easy file sharing from the command line`
	app.Version = Version
	app.Flags = globalFlags
	app.CustomAppHelpTemplate = helpTemplate
	app.Commands = []*cli.Command{
		{
			Name:   "version",
			Action: versionCommand,
		},
	}

	app.Before = func(c *cli.Context) error {
		return nil
	}

	app.Action = func(c *cli.Context) error {
		var options []server.OptionFn
		if v := c.String("listener"); v != "" {
			options = append(options, server.Listener(v))
		}

		if v := c.String("cors-domains"); v != "" {
			options = append(options, server.CorsDomains(v))
		}

		if v := c.String("tls-listener"); v == "" {
		} else if c.Bool("tls-listener-only") {
			options = append(options, server.TLSListener(v, true))
		} else {
			options = append(options, server.TLSListener(v, false))
		}

		if v := c.String("profile-listener"); v != "" {
			options = append(options, server.ProfileListener(v))
		}

		if v := c.String("web-path"); v != "" {
			options = append(options, server.WebPath(v))
		}

		if v := c.String("proxy-path"); v != "" {
			options = append(options, server.ProxyPath(v))
		}

		if v := c.String("proxy-port"); v != "" {
			options = append(options, server.ProxyPort(v))
		}

		if v := c.String("email-contact"); v != "" {
			options = append(options, server.EmailContact(v))
		}

		if v := c.String("ga-key"); v != "" {
			options = append(options, server.GoogleAnalytics(v))
		}

		if v := c.String("uservoice-key"); v != "" {
			options = append(options, server.UserVoice(v))
		}

		if v := c.String("temp-path"); v != "" {
			options = append(options, server.TempPath(v))
		}

		if v := c.String("log"); v != "" {
			options = append(options, server.LogFile(logger, v))
		} else {
			options = append(options, server.Logger(logger))
		}

		if v := c.String("lets-encrypt-hosts"); v != "" {
			options = append(options, server.UseLetsEncrypt(strings.Split(v, ",")))
		}

		if v := c.String("virustotal-key"); v != "" {
			options = append(options, server.VirustotalKey(v))
		}

		if v := c.String("clamav-host"); v != "" {
			options = append(options, server.ClamavHost(v))
		}

		if v := c.Bool("perform-clamav-prescan"); v {
			if c.String("clamav-host") == "" {
				return errors.New("clamav-host not set")
			}

			options = append(options, server.PerformClamavPrescan(v))
		}

		if v := c.Int64("max-upload-size"); v > 0 {
			options = append(options, server.MaxUploadSize(v))
		}

		if v := c.Int("rate-limit"); v > 0 {
			options = append(options, server.RateLimit(v))
		}

		v := c.Int("random-token-length")
		options = append(options, server.RandomTokenLength(v))

		purgeDays := c.Int("purge-days")
		purgeInterval := c.Int("purge-interval")
		if purgeDays > 0 && purgeInterval > 0 {
			options = append(options, server.Purge(purgeDays, purgeInterval))
		}

		if cert := c.String("tls-cert-file"); cert == "" {
		} else if pk := c.String("tls-private-key"); pk == "" {
		} else {
			options = append(options, server.TLSConfig(cert, pk))
		}

		if c.Bool("profiler") {
			options = append(options, server.EnableProfiler())
		}

		if c.Bool("force-https") {
			options = append(options, server.ForceHTTPS())
		}

		if httpAuthUser := c.String("http-auth-user"); httpAuthUser == "" {
		} else if httpAuthPass := c.String("http-auth-pass"); httpAuthPass == "" {
		} else {
			options = append(options, server.HTTPAuthCredentials(httpAuthUser, httpAuthPass))
		}

		if httpAuthHtpasswd := c.String("http-auth-htpasswd"); httpAuthHtpasswd != "" {
			options = append(options, server.HTTPAuthHtpasswd(httpAuthHtpasswd))
		}

		if httpAuthIPWhitelist := c.String("http-auth-ip-whitelist"); httpAuthIPWhitelist != "" {
			ipFilterOptions := server.IPFilterOptions{}
			ipFilterOptions.AllowedIPs = strings.Split(httpAuthIPWhitelist, ",")
			ipFilterOptions.BlockByDefault = true
			options = append(options, server.HTTPAUTHFilterOptions(ipFilterOptions))
		}

		applyIPFilter := false
		ipFilterOptions := server.IPFilterOptions{}
		if ipWhitelist := c.String("ip-whitelist"); ipWhitelist != "" {
			applyIPFilter = true
			ipFilterOptions.AllowedIPs = strings.Split(ipWhitelist, ",")
			ipFilterOptions.BlockByDefault = true
		}

		if ipBlacklist := c.String("ip-blacklist"); ipBlacklist != "" {
			applyIPFilter = true
			ipFilterOptions.BlockedIPs = strings.Split(ipBlacklist, ",")
		}

		if applyIPFilter {
			options = append(options, server.FilterOptions(ipFilterOptions))
		}

		switch provider := c.String("provider"); provider {
		case "s3":
			if accessKey := c.String("aws-access-key"); accessKey == "" {
				return errors.New("access-key not set.")
			} else if secretKey := c.String("aws-secret-key"); secretKey == "" {
				return errors.New("secret-key not set.")
			} else if bucket := c.String("bucket"); bucket == "" {
				return errors.New("bucket not set.")
			} else if store, err := storage.NewS3Storage(c.Context, accessKey, secretKey, bucket, purgeDays, c.String("s3-region"), c.String("s3-endpoint"), c.Bool("s3-no-multipart"), c.Bool("s3-path-style"), logger); err != nil {
				return err
			} else {
				options = append(options, server.UseStorage(store))
			}
		case "gdrive":
			chunkSize := c.Int("gdrive-chunk-size") * 1024 * 1024

			if clientJSONFilepath := c.String("gdrive-client-json-filepath"); clientJSONFilepath == "" {
				return errors.New("gdrive-client-json-filepath not set.")
			} else if localConfigPath := c.String("gdrive-local-config-path"); localConfigPath == "" {
				return errors.New("gdrive-local-config-path not set.")
			} else if basedir := c.String("basedir"); basedir == "" {
				return errors.New("basedir not set.")
			} else if store, err := storage.NewGDriveStorage(c.Context, clientJSONFilepath, localConfigPath, basedir, chunkSize, logger); err != nil {
				return err
			} else {
				options = append(options, server.UseStorage(store))
			}
		case "storj":
			if access := c.String("storj-access"); access == "" {
				return errors.New("storj-access not set.")
			} else if bucket := c.String("storj-bucket"); bucket == "" {
				return errors.New("storj-bucket not set.")
			} else if store, err := storage.NewStorjStorage(c.Context, access, bucket, purgeDays, logger); err != nil {
				return err
			} else {
				options = append(options, server.UseStorage(store))
			}
		case "local":
			if v := c.String("basedir"); v == "" {
				return errors.New("basedir not set.")
			} else if store, err := storage.NewLocalStorage(v, logger); err != nil {
				return err
			} else {
				options = append(options, server.UseStorage(store))
			}
		default:
			return errors.New("Provider not set or invalid.")
		}

		srvr, err := server.New(
			options...,
		)

		if err != nil {
			logger.Println(color.RedString("Error starting server: %s", err.Error()))
			return err
		}

		srvr.Run()
		return nil
	}

	return &Cmd{
		App: app,
	}
}


================================================
FILE: examples.md
================================================
# Table of Contents

* [Aliases](#aliases)
* [Uploading and downloading](#uploading-and-downloading)
* [Archiving and backups](#archiving-and-backups)
* [Encrypting and decrypting](#encrypting-and-decrypting)
* [Scanning for viruses](#scanning-for-viruses)
* [Uploading and copy download command](#uploading-and-copy-download-command)
* [Uploading and displaying URL and deletion token](#uploading-and-displaying-url-and-deletion-token)

## Aliases
<a name="aliases"/>

## Add alias to .bashrc or .zshrc

### Using curl
```bash
transfer() {
    curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename "$1") | tee /dev/null;
    echo
}

alias transfer=transfer
```

### Using wget
```bash
transfer() {
    wget -t 1 -qO - --method=PUT --body-file="$1" --header="Content-Type: $(file -b --mime-type "$1")" https://transfer.sh/$(basename "$1");
    echo
}

alias transfer=transfer
```

## Add alias for fish-shell

### Using curl
```fish
function transfer --description 'Upload a file to transfer.sh'
    if [ $argv[1] ]
        # write to output to tmpfile because of progress bar
        set -l tmpfile ( mktemp -t transferXXXXXX )
        curl --progress-bar --upload-file "$argv[1]" https://transfer.sh/(basename $argv[1]) >> $tmpfile
        cat $tmpfile
        command rm -f $tmpfile
    else
        echo 'usage: transfer FILE_TO_TRANSFER'
    end
end

funcsave transfer
```

### Using wget
```fish
function transfer --description 'Upload a file to transfer.sh'
    if [ $argv[1] ]
        wget -t 1 -qO - --method=PUT --body-file="$argv[1]" --header="Content-Type: (file -b --mime-type $argv[1])" https://transfer.sh/(basename $argv[1])
    else
        echo 'usage: transfer FILE_TO_TRANSFER'
    end
end

funcsave transfer
```

Now run it like this:
```bash
$ transfer test.txt
```

## Add alias on Windows

Put a file called `transfer.cmd` somewhere in your PATH with this inside it:
```cmd
@echo off
setlocal
:: use env vars to pass names to PS, to avoid escaping issues
set FN=%~nx1
set FULL=%1
powershell -noprofile -command "$(Invoke-Webrequest -Method put -Infile $Env:FULL https://transfer.sh/$Env:FN).Content"
```

## Uploading and Downloading
<a name="uploading-and-downloading"/>

### Uploading with wget
```bash
$ wget --method PUT --body-file=/tmp/file.tar https://transfer.sh/file.tar -O - -nv 
```

### Uploading with PowerShell
```posh
PS H:\> invoke-webrequest -method put -infile .\file.txt https://transfer.sh/file.txt 
```

### Upload using HTTPie
```bash
$ http https://transfer.sh/ -vv < /tmp/test.log 
```

### Uploading a filtered text file
```bash
$ grep 'pound' /var/log/syslog | curl --upload-file - https://transfer.sh/pound.log 
```

### Downloading with curl
```bash
$ curl https://transfer.sh/1lDau/test.txt -o test.txt
```

### Downloading with wget
```bash
$ wget https://transfer.sh/1lDau/test.txt
```

## Archiving and backups
<a name="archiving-and-backups"/>

### Backup, encrypt and transfer a MySQL dump
```bash
$ mysqldump --all-databases | gzip | gpg -ac -o- | curl -X PUT --upload-file "-" https://transfer.sh/test.txt
```

### Archive and upload directory
```bash
$ tar -czf - /var/log/journal | curl --upload-file - https://transfer.sh/journal.tar.gz
```

### Uploading multiple files at once
```bash
$ curl -i -F filedata=@/tmp/hello.txt -F filedata=@/tmp/hello2.txt https://transfer.sh/
```

### Combining downloads as zip or tar.gz archive
```bash
$ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).tar.gz
$ curl https://transfer.sh/(15HKz/hello.txt,15HKz/hello.txt).zip 
```

### Transfer and send email with link (using an alias)
```bash
$ transfer /tmp/hello.txt | mail -s "Hello World" user@yourmaildomain.com 
```
## Encrypting and decrypting
<a name="encrypting-and-decrypting"/>

### Encrypting files with password using gpg
```bash
$ gpg --armor --symmetric --output - /tmp/hello.txt | curl --upload-file - https://transfer.sh/test.txt
```

### Downloading and decrypting
```bash
$ curl https://transfer.sh/1lDau/test.txt | gpg --decrypt --output /tmp/hello.txt
```

### Import keys from [keybase](https://keybase.io/)
```bash
$ keybase track [them] # Encrypt for recipient(s)
$ cat somebackupfile.tar.gz | keybase encrypt [them] | curl --upload-file '-' https://transfer.sh/test.txt # Decrypt
$ curl https://transfer.sh/sqUFi/test.md | keybase decrypt
```

## Scanning for viruses
<a name="scanning-for-viruses"/>

### Scan for malware or viruses using Clamav
```bash
$ wget http://www.eicar.org/download/eicar.com
$ curl -X PUT --upload-file ./eicar.com https://transfer.sh/eicar.com/scan
```

### Upload malware to VirusTotal, get a permalink in return
```bash
$ curl -X PUT --upload-file nhgbhhj https://transfer.sh/test.txt/virustotal 
```

### Upload encrypted password protected files

By default files upload for only 1 download, you can specify download limit using -D flag like `transfer-encrypted -D 50 %file/folder%`

#### One line for bashrc
```bash
transfer-encrypted() { if [ $# -eq 0 ]; then echo "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>" >&2; return 1; fi; while getopts ":D:" opt; do case $opt in D) max_downloads=$OPTARG;; \?) echo "Invalid option: -$OPTARG" >&2;; esac; done; shift "$((OPTIND - 1))"; file="$1"; file_name=$(basename "$file"); if [ ! -e "$file" ]; then echo "$file: No such file or directory" >&2; return 1; fi; if [ -d "$file" ]; then file_name="$file_name.zip"; (cd "$file" && zip -r -q - .) | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file_name" && cat "tmp-$file_name" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file "tmp-$file_name" "https://transfer.sh/$file_name" | tee /dev/null; rm "tmp-$file_name"; else cat "$file" | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file" && cat "tmp-$file" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file - "https://transfer.sh/$file_name" | tee /dev/null; rm "tmp-$file"; fi; }
```
#### Human readable code 
```bash
transfer-encrypted() {
    if [ $# -eq 0 ]; then
        echo "No arguments specified.\nUsage:\n transfer <file|directory>\n ... | transfer <file_name>" >&2
        return 1
    fi

    while getopts ":D:" opt; do
        case $opt in
            D)
                max_downloads=$OPTARG
                ;;
            \?)
                echo "Invalid option: -$OPTARG" >&2
                ;;
        esac
    done

    shift "$((OPTIND - 1))"
    file="$1"
    file_name=$(basename "$file")

    if [ ! -e "$file" ]; then
        echo "$file: No such file or directory" >&2
        return 1
    fi

    if [ -d "$file" ]; then
        file_name="$file_name.zip"
        (cd "$file" && zip -r -q - .) | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file_name" && cat "tmp-$file_name" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file "tmp-$file_name" "https://transfer.sh/$file_name" | tee /dev/null
        rm "tmp-$file_name"
    else
        cat "$file" | openssl aes-256-cbc -pbkdf2 -e > "tmp-$file" && cat "tmp-$file" | curl -H "Max-Downloads: $max_downloads" -w '\n' --upload-file - "https://transfer.sh/$file_name" | tee /dev/null
        rm "tmp-$file"
    fi
}
```
#### Decrypt using
```bash
curl -s https://transfer.sh/some/file | openssl aes-256-cbc -pbkdf2 -d > output_filename
```

## Uploading and copy download command

Download commands can be automatically copied to the clipboard after files are uploaded using transfer.sh.

It was designed for Linux or macOS.

### 1. Install xclip or xsel for Linux, macOS skips this step

- install xclip see https://command-not-found.com/xclip

- install xsel  see https://command-not-found.com/xsel

Install later, add pbcopy and pbpaste to .bashrc or .zshrc or its equivalent.

- If use xclip, paste the following lines:

```sh
alias pbcopy='xclip -selection clipboard'
alias pbpaste='xclip -selection clipboard -o'
```

- If use xsel, paste the following lines:

```sh
alias pbcopy='xsel --clipboard --input'
alias pbpaste='xsel --clipboard --output'
```

### 2. Add Uploading and copy download command shell function

1. Open .bashrc or .zshrc  or its equivalent.

2. Add the following shell script:

   ```sh
   transfer() {
     curl --progress-bar --upload-file "$1" https://transfer.sh/$(basename "$1") | pbcopy;
     echo "1) Download link:"
     echo "$(pbpaste)"
   
     echo "\n2) Linux or macOS download command:"
     linux_macos_download_command="wget $(pbpaste)"
     echo $linux_macos_download_command
   
     echo "\n3) Windows download command:"
     windows_download_command="Invoke-WebRequest -Uri "$(pbpaste)" -OutFile $(basename $1)"
     echo $windows_download_command
   
     case $2 in
       l|m)  echo $linux_macos_download_command | pbcopy
       ;;
       w)  echo $windows_download_command | pbcopy
       ;;
     esac
   }
   ```


### 3. Test

The transfer command has two parameters:

1. The first parameter is the path to upload the file.

2. The second parameter indicates which system's download command is copied. optional:

   - This parameter is empty to copy the download link.

   - `l` or `m` copy the Linux or macOS command that downloaded the file.

   -  `w` copy the Windows command that downloaded the file.

For example, The command to download the file on Windows will be copied:

```sh
$ transfer ~/temp/a.log w
######################################################################## 100.0%
1) Download link:
https://transfer.sh/y0qr2c/a.log

2) Linux or macOS download command:
wget https://transfer.sh/y0qr2c/a.log

3) Windows download command:
Invoke-WebRequest -Uri https://transfer.sh/y0qr2c/a.log -OutFile a.log
```
## Uploading and displaying URL and deletion token
```bash
# tempfile
URLFILE=$HOME/temp/transfersh.url
# insert number of downloads and days saved
if [ -f $1 ]; then
read -p "Allowed number of downloads: " num_down
read -p "Number of days on server: " num_save
# transfer
curl -sD - -H "Max-Downloads: $num_down" -H "Max-Days: $num_save"--progress-bar --upload-file $1 https://transfer.sh/$(basename $1) | grep -i -E 'transfer\.sh|x-url-delete' &> $URLFILE
# display URL and deletion token
if [ -f $URLFILE ]; then
URL=$(tail -n1 $URLFILE)
TOKEN=$(grep delete $URLFILE | awk -F "/" '{print $NF}')
echo "*********************************"
echo "Data is saved in $URLFILE"
echo "**********************************"
echo "URL is: $URL"
echo "Deletion Token is: $TOKEN"
echo "**********************************"
else
echo "NO URL-File found !!"
fi
else
echo "!!!!!!"
echo "\"$1\" not found !!"
echo "!!!!!!"
fi
```


================================================
FILE: extras/clamd
================================================
#! /bin/sh
### BEGIN INIT INFO
# Provides:          skeleton
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# Author: Foo Bar <foobar@baz.org>
#
# Please remove the "Author" lines above and replace them
# with your own name if you copy and modify this script.

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin
DESC="Clam Daemon"
NAME=clamd
DAEMON="/usr/local/sbin/clamd"
DAEMON_ARGS="-c /usr/local/etc/clamd.conf"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
	# Return
	#   0 if daemon has been started
	#   1 if daemon was already running
	#   2 if daemon could not be started
	start-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON --test > /dev/null \
		|| return 1
	start-stop-daemon --background --start --quiet --pidfile $PIDFILE --exec $DAEMON -- \
		$DAEMON_ARGS \
		|| return 2
	# Add code here, if necessary, that waits for the process to be ready
	# to handle requests from services started subsequently which depend
	# on this one.  As a last resort, sleep for some time.
}

#
# Function that stops the daemon/service
#
do_stop()
{
	# Return
	#   0 if daemon has been stopped
	#   1 if daemon was already stopped
	#   2 if daemon could not be stopped
	#   other if a failure occurred
	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
	RETVAL="$?"
	[ "$RETVAL" = 2 ] && return 2
	# Wait for children to finish too if this is a daemon that forks
	# and if the daemon is only ever run from this initscript.
	# If the above conditions are not satisfied then add some other code
	# that waits for the process to drop all resources that could be
	# needed by services started subsequently.  A last resort is to
	# sleep for some time.
	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
	[ "$?" = 2 ] && return 2
	# Many daemons don't delete their pidfiles when they exit.
	rm -f $PIDFILE
	return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
	#
	# If the daemon can reload its configuration without
	# restarting (for example, when it is sent a SIGHUP),
	# then implement that here.
	#
	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
	return 0
}

case "$1" in
  start)
	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
	do_start
	case "$?" in
		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
	esac
	;;
  stop)
	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
	do_stop
	case "$?" in
		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
	esac
	;;
  status)
	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
	;;
  #reload|force-reload)
	#
	# If do_reload() is not implemented then leave this commented out
	# and leave 'force-reload' as an alias for 'restart'.
	#
	#log_daemon_msg "Reloading $DESC" "$NAME"
	#do_reload
	#log_end_msg $?
	#;;
  restart|force-reload)
	#
	# If the "reload" option is implemented then remove the
	# 'force-reload' alias
	#
	log_daemon_msg "Restarting $DESC" "$NAME"
	do_stop
	case "$?" in
	  0|1)
		do_start
		case "$?" in
			0) log_end_msg 0 ;;
			1) log_end_msg 1 ;; # Old process is still running
			*) log_end_msg 1 ;; # Failed to start
		esac
		;;
	  *)
		# Failed to stop
		log_end_msg 1
		;;
	esac
	;;
  *)
	#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
	exit 3
	;;
esac

:


================================================
FILE: extras/transfersh
================================================
#! /bin/sh
### BEGIN INIT INFO
# Provides:          skeleton
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: Example initscript
# Description:       This file should be used to construct scripts to be
#                    placed in /etc/init.d.
### END INIT INFO

# Author: Foo Bar <foobar@baz.org>
#
# Please remove the "Author" lines above and replace them
# with your own name if you copy and modify this script.

# Do NOT "set -e"

# PATH should only include /usr/* if it runs after the mountnfs.sh script
PATH=/sbin:/usr/sbin:/bin:/usr/bin:/usr/local/go/bin
DESC="Transfersh Web server"
NAME=transfersh
DAEMON="/opt/transfer.sh/main"
DAEMON_ARGS="--port 80 --temp /tmp/"
PIDFILE=/var/run/$NAME.pid
SCRIPTNAME=/etc/init.d/$NAME

export BUCKET={bucket}
export AWS_ACCESS_KEY={aws_access_key}
export AWS_SECRET_KEY={aws_secret_key}
export VIRUSTOTAL_KEY={virustotal_key}
export GOPATH=/opt/go/

# Exit if the package is not installed
[ -x "$DAEMON" ] || exit 0

# Read configuration variable file if it is present
[ -r /etc/default/$NAME ] && . /etc/default/$NAME

# Load the VERBOSE setting and other rcS variables
. /lib/init/vars.sh

# Define LSB log_* functions.
# Depend on lsb-base (>= 3.2-14) to ensure that this file is present
# and status_of_proc is working.
. /lib/lsb/init-functions

#
# Function that starts the daemon/service
#
do_start()
{
	# Return
	#   0 if daemon has been started
	#   1 if daemon was already running
	#   2 if daemon could not be started
	start-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON --test > /dev/null \
		|| return 1
	start-stop-daemon --background --start --chdir /opt/transfer.sh --quiet --pidfile $PIDFILE --make-pidfile --exec $DAEMON -- \
		$DAEMON_ARGS \
		|| return 2
	# Add code here, if necessary, that waits for the process to be ready
	# to handle requests from services started subsequently which depend
	# on this one.  As a last resort, sleep for some time.
}

#
# Function that stops the daemon/service
#
do_stop()
{
	# Return
	#   0 if daemon has been stopped
	#   1 if daemon was already stopped
	#   2 if daemon could not be stopped
	#   other if a failure occurred
	start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE --name $NAME
	RETVAL="$?"
	[ "$RETVAL" = 2 ] && return 2
	# Wait for children to finish too if this is a daemon that forks
	# and if the daemon is only ever run from this initscript.
	# If the above conditions are not satisfied then add some other code
	# that waits for the process to drop all resources that could be
	# needed by services started subsequently.  A last resort is to
	# sleep for some time.
	start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON
	[ "$?" = 2 ] && return 2
	# Many daemons don't delete their pidfiles when they exit.
	rm -f $PIDFILE
	return "$RETVAL"
}

#
# Function that sends a SIGHUP to the daemon/service
#
do_reload() {
	#
	# If the daemon can reload its configuration without
	# restarting (for example, when it is sent a SIGHUP),
	# then implement that here.
	#
	start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --name $NAME
	return 0
}

case "$1" in
  start)
	[ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME"
	do_start
	case "$?" in
		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
	esac
	;;
  stop)
	[ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME"
	do_stop
	case "$?" in
		0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;;
		2) [ "$VERBOSE" != no ] && log_end_msg 1 ;;
	esac
	;;
  status)
	status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $?
	;;
  #reload|force-reload)
	#
	# If do_reload() is not implemented then leave this commented out
	# and leave 'force-reload' as an alias for 'restart'.
	#
	#log_daemon_msg "Reloading $DESC" "$NAME"
	#do_reload
	#log_end_msg $?
	#;;
  restart|force-reload)
	#
	# If the "reload" option is implemented then remove the
	# 'force-reload' alias
	#
	log_daemon_msg "Restarting $DESC" "$NAME"
	do_stop
	case "$?" in
	  0|1)
		do_start
		case "$?" in
			0) log_end_msg 0 ;;
			1) log_end_msg 1 ;; # Old process is still running
			*) log_end_msg 1 ;; # Failed to start
		esac
		;;
	  *)
		# Failed to stop
		log_end_msg 1
		;;
	esac
	;;
  *)
	#echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2
	echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2
	exit 3
	;;
esac

:


================================================
FILE: flake.nix
================================================
{
  description = "Transfer.sh";

  inputs.flake-utils.url = "github:numtide/flake-utils";

  outputs = { self, nixpkgs, flake-utils }:
    let
      transfer-sh = pkgs: pkgs.buildGoModule {
        src = self;
        name = "transfer.sh";
        vendorSha256 = "sha256-bgQUMiC33yVorcKOWhegT1/YU+fvxsz2pkeRvjf3R7g=";
      };
    in

      flake-utils.lib.eachDefaultSystem (
        system:
          let
            pkgs = nixpkgs.legacyPackages.${system};
          in
            rec {
              packages = flake-utils.lib.flattenTree {
                transfer-sh = transfer-sh pkgs;
              };
              defaultPackage = packages.transfer-sh;
              apps.transfer-sh = flake-utils.lib.mkApp { drv = packages.transfer-sh; };
              defaultApp = apps.transfer-sh;
            }
      ) // rec {

        nixosModules = {
          transfer-sh = { config, lib, pkgs, ... }: with lib; let
            RUNTIME_DIR = "/var/lib/transfer.sh";
            cfg = config.services.transfer-sh;

            general_options = {

              enable = mkEnableOption "Transfer.sh service";
              listener = mkOption { default = 80; type = types.int; description = "port to use for http (:80)"; };
              profile-listener = mkOption { default = 6060; type = types.int; description = "port to use for profiler (:6060)"; };
              force-https = mkOption { type = types.nullOr types.bool; description = "redirect to https"; };
              tls-listener = mkOption { default = 443; type = types.int; description = "port to use for https (:443)"; };
              tls-listener-only = mkOption { type = types.nullOr types.bool; description = "flag to enable tls listener only"; };
              tls-cert-file = mkOption { type = types.nullOr types.str; description = "path to tls certificate"; };
              tls-private-key = mkOption { type = types.nullOr types.str; description = "path to tls private key "; };
              http-auth-user = mkOption { type = types.nullOr types.str; description = "user for basic http auth on upload"; };
              http-auth-pass = mkOption { type = types.nullOr types.str; description = "pass for basic http auth on upload"; };
              http-auth-htpasswd = mkOption { type = types.nullOr types.str; description = "htpasswd file path for basic http auth on upload"; };
              http-auth-ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to upload without being challenged an http auth"; };
              ip-whitelist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips allowed to connect to the service"; };
              ip-blacklist = mkOption { type = types.nullOr types.str; description = "comma separated list of ips not allowed to connect to the service"; };
              temp-path = mkOption { type = types.nullOr types.str; description = "path to temp folder"; };
              web-path = mkOption { type = types.nullOr types.str; description = "path to static web files (for development or custom front end)"; };
              proxy-path = mkOption { type = types.nullOr types.str; description = "path prefix when service is run behind a proxy"; };
              proxy-port = mkOption { type = types.nullOr types.str; description = "port of the proxy when the service is run behind a proxy"; };
              ga-key = mkOption { type = types.nullOr types.str; description = "google analytics key for the front end"; };
              email-contact = mkOption { type = types.nullOr types.str; description = "email contact for the front end"; };
              uservoice-key = mkOption { type = types.nullOr types.str; description = "user voice key for the front end"; };
              lets-encrypt-hosts = mkOption { type = types.nullOr (types.listOf types.str); description = "hosts to use for lets encrypt certificates"; };
              log = mkOption { type = types.nullOr types.str; description = "path to log file"; };
              cors-domains = mkOption { type = types.nullOr (types.listOf types.str); description = "comma separated list of domains for CORS, setting it enable CORS "; };
              clamav-host = mkOption { type = types.nullOr types.str; description = "host for clamav feature"; };
              rate-limit = mkOption { type = types.nullOr types.int; description = "request per minute"; };
              max-upload-size = mkOption { type = types.nullOr types.int; description = "max upload size in kilobytes  "; };
              purge-days = mkOption { type = types.nullOr types.int; description = "number of days after the uploads are purged automatically "; };
              random-token-length = mkOption { type = types.nullOr types.int; description = "length of the random token for the upload path (double the size for delete path)"; };

            };

            provider_options = {

                aws = {
                  enable = mkEnableOption "Enable AWS backend";
                  aws-access-key = mkOption { type = types.str; description = "aws access key"; };
                  aws-secret-key = mkOption { type = types.str; description = "aws secret key"; };
                  bucket = mkOption { type = types.str; description = "aws bucket "; };
                  s3-endpoint = mkOption {
                    type = types.nullOr types.str;
                    description = ''
                      Custom S3 endpoint. 
                      If you specify the s3-region, you don't need to set the endpoint URL since the correct endpoint will used automatically.
                    '';
                  };
                  s3-region = mkOption { type = types.str; description = "region of the s3 bucket eu-west-"; };
                  s3-no-multipart = mkOption { type = types.nullOr types.bool; description = "disables s3 multipart upload "; };
                  s3-path-style = mkOption { type = types.nullOr types.str; description = "Forces path style URLs, required for Minio. "; };
                };

                storj = {
                  enable = mkEnableOption "Enable storj backend";
                  storj-access = mkOption { type = types.str; description = "Access for the project"; };
                  storj-bucket = mkOption { type = types.str; description = "Bucket to use within the project"; };
                };

                gdrive = {
                  enable = mkEnableOption "Enable gdrive backend";
                  gdrive-client-json = mkOption { type = types.str; description = "oauth client json config for gdrive provider"; };
                  gdrive-chunk-size = mkOption { default = 8; type = types.nullOr types.int; description = "chunk size for gdrive upload in megabytes, must be lower than available memory (8 MB)"; };
                  basedir = mkOption { type = types.str; description = "path storage for gdrive provider"; default = "${cfg.stateDir}/store"; };
                  purge-interval = mkOption { type = types.nullOr types.int; description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; };

                };

                local = {
                  enable = mkEnableOption "Enable local backend";
                  basedir = mkOption { type = types.str; description = "path storage for local provider"; default = "${cfg.stateDir}/store"; };
                  purge-interval = mkOption { type = types.nullOr types.int; description = "interval in hours to run the automatic purge for (not applicable to S3 and Storj)"; };
                };

              };
          in
            {
              options.services.transfer-sh = fold recursiveUpdate {} [
                general_options
                {
                  provider = provider_options;
                  user = mkOption {
                    type = types.str;
                    description = "User to run the service under";
                    default = "transfer.sh";
                  };
                  group = mkOption {
                    type = types.str;
                    description = "Group to run the service under";
                    default = "transfer.sh";
                  };
                  stateDir = mkOption {
                    type = types.path;
                    description = "Variable state directory";
                    default = RUNTIME_DIR;
                  };
                }
              ];

              config = let

                mkFlags = cfg: options:
                  let
                    mkBoolFlag = option: if cfg.${option} then [ "--${option}" ] else [];
                    mkFlag = option:
                      if isBool cfg.${option}
                      then mkBoolFlag option
                      else [ "--${option}" "${cfg.${option}}" ];

                  in
                    lists.flatten (map (mkFlag) (filter (option: cfg.${option} != null && option != "enable") options));

                aws-config = (mkFlags cfg.provider.aws (attrNames provider_options)) ++ [ "--provider" "aws" ];
                gdrive-config = mkFlags cfg.provider.gdrive (attrNames provider_options.gdrive) ++ [ "--provider" "gdrive" ];
                storj-config = mkFlags cfg.provider.storj (attrNames provider_options.storj) ++ [ "--provider" "storj" ];
                local-config = mkFlags cfg.provider.local (attrNames provider_options.local) ++ [ "--provider" "local" ];

                general-config = concatStringsSep " " (mkFlags cfg (attrNames general_options));
                provider-config = concatStringsSep " " (
                  if cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then aws-config
                  else if !cfg.provider.aws.enable && cfg.provider.storj.enable && !cfg.provider.gdrive.enable && !cfg.provider.local.enable then storj-config
                  else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && cfg.provider.gdrive.enable && !cfg.provider.local.enable then gdrive-config
                  else if !cfg.provider.aws.enable && !cfg.provider.storj.enable && !cfg.provider.gdrive.enable && cfg.provider.local.enable then local-config
                  else throw "transfer.sh requires exactly one provider (aws, storj, gdrive, local)"
                );

              in
                lib.mkIf cfg.enable
                  {
                    systemd.tmpfiles.rules = [
                      "d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.group} - -"
                    ] ++ optional cfg.provider.gdrive.enable cfg.provider.gdrive.basedir
                    ++ optional cfg.provider.local.enable cfg.provider.local.basedir;

                    systemd.services.transfer-sh = {
                      wantedBy = [ "multi-user.target" ];
                      after = [ "network.target" ];
                      serviceConfig = {
                        User = cfg.user;
                        Group = cfg.group;
                        ExecStart = "${transfer-sh pkgs}/bin/transfer.sh ${general-config} ${provider-config} ";
                      };
                    };

                    networking.firewall.allowedTCPPorts = [ cfg.listener cfg.profile-listener cfg.tls-listener ];
                  };
            };

          default = { self, pkgs, ... }: {
            imports = [ nixosModules.transfer-sh ];
            # Network configuration.

            # useDHCP is generally considered to better be turned off in favor
            # of <adapter>.useDHCP
            networking.useDHCP = false;
            networking.firewall.allowedTCPPorts = [];

            # Enable the inventaire server.
            services.transfer-sh = {
              enable = true;
              provider.local = {
                enable = true;
              };
            };

            nixpkgs.config.allowUnfree = true;
          };
        };


        nixosConfigurations."container" = nixpkgs.lib.nixosSystem {
          system = "x86_64-linux";
          modules = [
            nixosModules.default
            ({ ... }: { boot.isContainer = true; })
          ];
        };

      };
}


================================================
FILE: go.mod
================================================
module github.com/dutchcoders/transfer.sh

go 1.22.0

require (
	github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8
	github.com/ProtonMail/gopenpgp/v2 v2.5.2
	github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14
	github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2
	github.com/aws/aws-sdk-go-v2 v1.18.0
	github.com/aws/aws-sdk-go-v2/config v1.18.25
	github.com/aws/aws-sdk-go-v2/credentials v1.13.24
	github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67
	github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1
	github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
	github.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8
	github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6
	github.com/elazarl/go-bindata-assetfs v1.0.1
	github.com/fatih/color v1.14.1
	github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f
	github.com/gorilla/handlers v1.5.1
	github.com/gorilla/mux v1.8.0
	github.com/microcosm-cc/bluemonday v1.0.23
	github.com/russross/blackfriday/v2 v2.1.0
	github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
	github.com/tg123/go-htpasswd v1.2.1
	github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce
	github.com/urfave/cli/v2 v2.25.3
	golang.org/x/crypto v0.21.0
	golang.org/x/net v0.23.0
	golang.org/x/oauth2 v0.7.0
	golang.org/x/text v0.14.0
	google.golang.org/api v0.114.0
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
	storj.io/common v0.0.0-20230301105927-7f966760c100
	storj.io/uplink v1.10.0
)

require (
	cloud.google.com/go/compute v1.19.1 // indirect
	cloud.google.com/go/compute/metadata v0.2.3 // indirect
	github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 // indirect
	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 // indirect
	github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 // indirect
	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 // indirect
	github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 // indirect
	github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 // indirect
	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 // indirect
	github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 // indirect
	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 // indirect
	github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 // indirect
	github.com/aws/smithy-go v1.13.5 // indirect
	github.com/aymerick/douceur v0.2.0 // indirect
	github.com/calebcase/tmpfile v1.0.3 // indirect
	github.com/cloudflare/circl v1.6.1 // indirect
	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
	github.com/felixge/httpsnoop v1.0.3 // indirect
	github.com/flynn/noise v1.0.0 // indirect
	github.com/garyburd/redigo v1.6.4 // indirect
	github.com/gogo/protobuf v1.3.2 // indirect
	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
	github.com/golang/protobuf v1.5.3 // indirect
	github.com/google/uuid v1.3.0 // indirect
	github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect
	github.com/googleapis/gax-go/v2 v2.7.1 // indirect
	github.com/gorilla/css v1.0.0 // indirect
	github.com/gorilla/securecookie v1.1.1 // indirect
	github.com/jmespath/go-jmespath v0.4.0 // indirect
	github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f // indirect
	github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 // indirect
	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
	github.com/kr/pretty v0.3.1 // indirect
	github.com/kr/text v0.2.0 // indirect
	github.com/mattn/go-colorable v0.1.13 // indirect
	github.com/mattn/go-isatty v0.0.17 // indirect
	github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d // indirect
	github.com/rogpeppe/go-internal v1.9.0 // indirect
	github.com/spacemonkeygo/monkit/v3 v3.0.19 // indirect
	github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect
	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
	github.com/zeebo/blake3 v0.2.3 // indirect
	github.com/zeebo/errs v1.3.0 // indirect
	go.opencensus.io v0.24.0 // indirect
	golang.org/x/sync v0.1.0 // indirect
	golang.org/x/sys v0.18.0 // indirect
	google.golang.org/appengine v1.6.7 // indirect
	google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect
	google.golang.org/grpc v1.56.3 // indirect
	google.golang.org/protobuf v1.33.0 // indirect
	gopkg.in/yaml.v2 v2.4.0 // indirect
	storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a // indirect
	storj.io/picobuf v0.0.1 // indirect
)


================================================
FILE: go.sum
================================================
cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys=
cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY=
cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE=
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM=
cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
github.com/ProtonMail/go-crypto v0.0.0-20230124153114-0acdc8ae009b/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/ProtonMail/go-mime v0.0.0-20221031134845-8fd9bc37cf08/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/gopenpgp/v2 v2.5.2 h1:97SjlWNAxXl9P22lgwgrZRshQdiEfAht0g3ZoiA1GCw=
github.com/ProtonMail/gopenpgp/v2 v2.5.2/go.mod h1:52qDaCnto6r+CoWbuU50T77XQt99lIs46HtHtvgFO3o=
github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14 h1:3zOOc7WdrATDXof+h/rBgMsg0sAmZIEVHft1UbWHh94=
github.com/PuerkitoBio/ghost v0.0.0-20160324114900-206e6e460e14/go.mod h1:+VFiaivV54Sa94ijzA/ZHQLoHuoUIS9hIqCK6f/76Zw=
github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2 h1:sIvihcW4qpN5qGSjmrsDDAbLpEq5tuHjJJfWY0Hud5Y=
github.com/VojtechVitek/ratelimit v0.0.0-20160722140851-dc172bc0f6d2/go.mod h1:3YwJE8rEisS9eraee0hygGG4G3gqX8H8Nyu+nPTUnGU=
github.com/aws/aws-sdk-go-v2 v1.18.0 h1:882kkTpSFhdgYRKVZ/VCgf7sd0ru57p2JCxz4/oN5RY=
github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
github.com/aws/aws-sdk-go-v2/config v1.18.25 h1:JuYyZcnMPBiFqn87L2cRppo+rNwgah6YwD3VuyvaW6Q=
github.com/aws/aws-sdk-go-v2/config v1.18.25/go.mod h1:dZnYpD5wTW/dQF0rRNLVypB396zWCcPiBIvdvSWHEg4=
github.com/aws/aws-sdk-go-v2/credentials v1.13.24 h1:PjiYyls3QdCrzqUN35jMWtUK1vqVZ+zLfdOa/UPFDp0=
github.com/aws/aws-sdk-go-v2/credentials v1.13.24/go.mod h1:jYPYi99wUOPIFi0rhiOvXeSEReVOzBqFNOX5bXYoG2o=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3 h1:jJPgroehGvjrde3XufFIJUZVK5A2L9a3KwSFgKy9n8w=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.3/go.mod h1:4Q0UFP0YJf0NrsEuEYHpM9fTSEVnD16Z3uyEF7J9JGM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67 h1:fI9/5BDEaAv/pv1VO1X1n3jfP9it+IGqWsCuuBQI8wM=
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.67/go.mod h1:zQClPRIwQZfJlZq6WZve+s4Tb4JW+3V6eS+4+KrYeP8=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33 h1:kG5eQilShqmJbv11XL1VpyDbaEJzWxd4zRiCG30GSn4=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.33/go.mod h1:7i0PF1ME/2eUPFcjkVIwq+DOygHEoK92t5cDqNgYbIw=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27 h1:vFQlirhuM8lLlpI7imKOMsjdQLuN9CPi+k44F/OFVsk=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.27/go.mod h1:UrHnn3QV/d0pBZ6QBAEQcqFLf8FAzLmoUfPVIueOvoM=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34 h1:gGLG7yKaXG02/jBlg210R7VgQIotiQntNhsCFejawx8=
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.34/go.mod h1:Etz2dj6UHYuw+Xw830KfzCfWGMzqvUTCjUj5b76GVDc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25 h1:AzwRi5OKKwo4QNqPf7TjeO+tK8AyOK3GVSwmRPo7/Cs=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.25/go.mod h1:SUbB4wcbSEyCvqBxv/O/IBf93RbEze7U7OnoTlpPB+g=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28 h1:vGWm5vTpMr39tEZfQeDiDAMgk+5qsnvRny3FjLpnH5w=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.28/go.mod h1:spfrICMD6wCAhjhzHuy6DOZZ+LAIY10UxhUmLzpJTTs=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27 h1:0iKliEXAcCa2qVtRs7Ot5hItA2MsufrphbRFlz1Owxo=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.27/go.mod h1:EOwBD4J4S5qYszS5/3DpkejfuK+Z5/1uzICfPaZLtqw=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2 h1:NbWkRxEEIRSCqxhsHQuMiTH7yo+JZW1gp8v3elSVMTQ=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.2/go.mod h1:4tfW5l4IAB32VWCDEBxCRtR9T4BWy4I4kr1spr8NgZM=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1 h1:O+9nAy9Bb6bJFTpeNFtd9UfHbgxO1o4ZDAM9rQp5NsY=
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.1/go.mod h1:J9kLNzEiHSeGMyN7238EjJmBpCniVzFda75Gxl/NqB8=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10 h1:UBQjaMTCKwyUYwiVnUt6toEJwGXsLBI6al083tpjJzY=
github.com/aws/aws-sdk-go-v2/service/sso v1.12.10/go.mod h1:ouy2P4z6sJN70fR3ka3wD3Ro3KezSxU6eKGQI2+2fjI=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10 h1:PkHIIJs8qvq0e5QybnZoG1K/9QTrLr9OsqCIo59jOBA=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.10/go.mod h1:AFvkxc8xfBe8XA+5St5XIHHrQQtkxqrRincx4hmMHOk=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0 h1:2DQLAKDteoEDI8zpCzqBMaZlJuoE9iTYD0gFmXVax9E=
github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jelyPW7NoZcZXLVpDsXunGDrk8=
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bradfitz/gomemcache v0.0.0-20170208213004-1952afaa557d/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/calebcase/tmpfile v1.0.3 h1:BZrOWZ79gJqQ3XbAQlihYZf/YCV0H4KPIdM5K5oMpJo=
github.com/calebcase/tmpfile v1.0.3/go.mod h1:UAUc01aHeC+pudPagY/lWvt2qS9ZO5Zzof6/tIUzqeI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI=
github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40=
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e h1:rcHHSQqzCgvlwP0I/fQ8rQMn/MpHE5gWSLdtpxtP6KQ=
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e/go.mod h1:Byz7q8MSzSPkouskHJhX0er2mZY/m0Vj5bMeMCkkyY4=
github.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8 h1:wEwYJxNLG29OesabDdAJWFBIO42HOL4x5kjvGuZLIyk=
github.com/Aetherinox/go-virustotal v0.0.0-20250520084801-0eb8c8f901c8/go.mod h1:myGG2GhfY2AgAPe8lFZw6Y1+IxhU+ED7ilotbpdQsDw=
github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6 h1:7uTRy44YpQi6/mtDq0N9zeQRCGEh93o7gKq/usGgpF8=
github.com/dutchcoders/transfer.sh-web v0.0.0-20221119114740-ca3a2621d2a6/go.mod h1:F6Q37CxDh2MHr5KXkcZmNB3tdkK7v+bgE+OpBY+9ilI=
github.com/elazarl/go-bindata-assetfs v1.0.1 h1:m0kkaHRKEu7tUIUFVwhGGGYClXvyl4RE03qmvRTNfbw=
github.com/elazarl/go-bindata-assetfs v1.0.1/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w=
github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ=
github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag=
github.com/fsnotify/fsnotify v1.4.3-0.20170329110642-4da3e2cfbabc/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.1.1-0.20170914051019-70e1b1943d4f/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/garyburd/redigo v1.6.4 h1:LFu2R3+ZOPgSMWMOL+saa/zXRjw0ID2G8FepO53BGlg=
github.com/garyburd/redigo v1.6.4/go.mod h1:rTb6epsqigu3kYKBnaF028A7Tf/Aw5s0cqA47doKKqw=
github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f/go.mod h1:ijRvpgDJDI262hYq/IQVYgf8hd8IHUs93Ol0kvMBAx4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/lint v0.0.0-20170918230701-e5d664eb928e/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.1.1-0.20171103154506-982329095285/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20211108044417-e9b028704de0 h1:rsq1yB2xiFLDYYaYdlGBsSkwVzsCo500wMhxvW5A/bk=
github.com/google/pprof v0.0.0-20211108044417-e9b028704de0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k=
github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k=
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A=
github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gregjones/httpcache v0.0.0-20170920190843-316c5e0ff04e/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/hashicorp/hcl v0.0.0-20170914154624-68e816d1c783/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/inconshreveable/log15 v0.0.0-20170622235902-74a0988b5f80/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f h1:HM2D/tnqbzNoN5DGIeB8ibM1BMYCkRWOqyWWcNAWw8o=
github.com/jtolio/eventkit v0.0.0-20230301123942-0cee1388f16f/go.mod h1:PXFUrknJu7TkBNyL8t7XWDPtDFFLFrNQQAdsXv9YfJE=
github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059 h1:4xdaxDg3xe+DKZC8NjbH/gvTs4iNYUnzOAiD5QL5NrM=
github.com/jtolio/noiseconn v0.0.0-20230227223919-bddcd1327059/go.mod h1:f0ijQHcvHYAuxX6JA/JUr/Z0FVn12D9REaT/HAWVgP4=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.7.4-0.20170902060319-8d7837e64d3c/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/mattn/go-colorable v0.0.10-0.20170816031813-ad5389df28cd/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.2/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/microcosm-cc/bluemonday v1.0.23 h1:SMZe2IGa0NuHvnVNAZ+6B38gsTbi5e4sViiWJyDDqFY=
github.com/microcosm-cc/bluemonday v1.0.23/go.mod h1:mN70sk7UkkF8TUr2IGBpNN0jAgStuPzlK76QuruE/z4=
github.com/mitchellh/mapstructure v0.0.0-20170523030023-d0303fe80992/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/pelletier/go-toml v1.0.1-0.20170904195809-1d6b12b7cb29/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shuLhan/go-bindata v4.0.0+incompatible/go.mod h1:pkcPAATLBDD2+SpAPnX5vEM90F7fcwHCvvLCMXcmw3g=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spacemonkeygo/monkit/v3 v3.0.19 h1:wqBb9bpD7jXkVi4XwIp8jn1fektaVBQ+cp9SHRXgAdo=
github.com/spacemonkeygo/monkit/v3 v3.0.19/go.mod h1:kj1ViJhlyADa7DiA4xVnTuPA46lFKbM7mxQTrXCuJP4=
github.com/spf13/afero v0.0.0-20170901052352-ee1bd8ee15a1/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.1.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/jwalterweatherman v0.0.0-20170901151539-12bd96e66386/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.1-0.20170901120850-7aff26db30c1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tg123/go-htpasswd v1.2.1 h1:i4wfsX1KvvkyoMiHZzjS0VzbAPWfxzI8INcZAKtutoU=
github.com/tg123/go-htpasswd v1.2.1/go.mod h1:erHp1B86KXdwQf1X5ZrLb7erXZnWueEQezb2dql4q58=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k=
github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/assert v1.3.1 h1:vukIABvugfNMZMQO1ABsyQDJDTVQbn+LWSMy1ol1h6A=
github.com/zeebo/assert v1.3.1/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs=
github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
golang.org/x/mobile v0.0.0-20221110043201-43a038452099/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.0.0-20170912212905-13449ad91cb2/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20170424234030-8be79e1e0910/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20170921000349-586095a6e407/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE=
google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20170918111702-1e559d0a00ee/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A=
google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU=
google.golang.org/grpc v1.2.1-0.20170921194603-d4b75ebd4f9f/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc=
google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
storj.io/common v0.0.0-20230301105927-7f966760c100 h1:0Rc6boo10ZgiHdadHi1o2OUv25YvTn8fSc/VyRz2Tyk=
storj.io/common v0.0.0-20230301105927-7f966760c100/go.mod h1:tDgoLthBVcrTPEokBgPdjrn39p/gyNx06j6ehhTSiUg=
storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a h1:FBaOc8c5efmW3tmPsiGy07USMkOSu/tyYCZpu2ro0y8=
storj.io/drpc v0.0.33-0.20230204035225-c9649dee8f2a/go.mod h1:6rcOyR/QQkSTX/9L5ZGtlZaE2PtXTTZl8d+ulSeeYEg=
storj.io/picobuf v0.0.1 h1:ekEvxSQCbEjTVIi/qxj2za13SJyfRE37yE30IBkZeT0=
storj.io/picobuf v0.0.1/go.mod h1:7ZTAMs6VesgTHbbhFU79oQ9hDaJ+MD4uoFQZ1P4SEz0=
storj.io/uplink v1.10.0 h1:3hS0hszupHSxEoC4DsMpljaRy0uNoijEPVF6siIE28Q=
storj.io/uplink v1.10.0/go.mod h1:gJIQumB8T3tBHPRive51AVpbc+v2xe+P/goFNMSRLG4=


================================================
FILE: main.go
================================================
package main

import (
	"log"
	"os"

	"github.com/dutchcoders/transfer.sh/cmd"
)

func main() {
	app := cmd.New()
	err := app.Run(os.Args)
	if err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: manifest.json
================================================
{
    "dependencies": {
        "github.com/dutchcoders/transfer.sh-web": {
            "branch": "master"
        }
    }
}


================================================
FILE: server/clamav.go
================================================
/*
The MIT License (MIT)

Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]
Copyright (c) 2018-2020 Andrea Spacca.
Copyright (c) 2020- Andrea Spacca and Stefan Benten.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package server

import (
	"errors"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"

	"github.com/dutchcoders/go-clamd"
	"github.com/gorilla/mux"
)

const clamavScanStatusOK = "OK"

func (s *Server) scanHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	filename := sanitize(vars["filename"])

	contentLength := r.ContentLength
	contentType := r.Header.Get("Content-Type")

	s.logger.Printf("Scanning %s %d %s", filename, contentLength, contentType)

	file, err := os.CreateTemp(s.tempPath, "clamav-")
	defer s.cleanTmpFile(file)
	if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	_, err = io.Copy(file, r.Body)
	if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	status, err := s.performScan(file.Name())
	if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	_, _ = w.Write([]byte(fmt.Sprintf("%v\n", status)))
}

func (s *Server) performScan(path string) (string, error) {
	c := clamd.NewClamd(s.ClamAVDaemonHost)

	responseCh := make(chan chan *clamd.ScanResult)
	errCh := make(chan error)
	go func(responseCh chan chan *clamd.ScanResult, errCh chan error) {
		response, err := c.ScanFile(path)
		if err != nil {
			errCh <- err
			return
		}

		responseCh <- response
	}(responseCh, errCh)

	select {
	case err := <-errCh:
		return "", err
	case response := <-responseCh:
		st := <-response
		return st.Status, nil
	case <-time.After(time.Second * 60):
		return "", errors.New("clamav scan timeout")
	}
}


================================================
FILE: server/handlers.go
================================================
/*
The MIT License (MIT)

Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]
Copyright (c) 2018-2020 Andrea Spacca.
Copyright (c) 2020- Andrea Spacca and Stefan Benten.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package server

import (
	"archive/tar"
	"archive/zip"
	"bytes"
	"compress/gzip"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"html"
	htmlTemplate "html/template"
	"io"
	"mime"
	"net"
	"net/http"
	"net/url"
	"os"
	"path"
	"path/filepath"
	"strconv"
	"strings"
	"sync"
	textTemplate "text/template"
	"time"
	"unicode"

	"github.com/ProtonMail/go-crypto/openpgp"
	"github.com/ProtonMail/go-crypto/openpgp/armor"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/ProtonMail/gopenpgp/v2/constants"
	"github.com/dutchcoders/transfer.sh/server/storage"
	"github.com/tg123/go-htpasswd"
	"github.com/tomasen/realip"

	web "github.com/dutchcoders/transfer.sh-web"
	"github.com/gorilla/mux"
	"github.com/microcosm-cc/bluemonday"
	blackfriday "github.com/russross/blackfriday/v2"
	qrcode "github.com/skip2/go-qrcode"
	"golang.org/x/net/idna"
	"golang.org/x/text/runes"
	"golang.org/x/text/transform"
	"golang.org/x/text/unicode/norm"
)

const getPathPart = "get"

var (
	htmlTemplates = initHTMLTemplates()
	textTemplates = initTextTemplates()
)

func stripPrefix(path string) string {
	return strings.Replace(path, web.Prefix+"/", "", -1)
}

func initTextTemplates() *textTemplate.Template {
	templateMap := textTemplate.FuncMap{"format": formatNumber}

	// Templates with functions available to them
	var templates = textTemplate.New("").Funcs(templateMap)
	return templates
}

func initHTMLTemplates() *htmlTemplate.Template {
	templateMap := htmlTemplate.FuncMap{"format": formatNumber}

	// Templates with functions available to them
	var templates = htmlTemplate.New("").Funcs(templateMap)

	return templates
}

func attachEncryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) {
	if len(password) == 0 {
		return reader, nil
	}

	return encrypt(reader, []byte(password))
}

func attachDecryptionReader(reader io.ReadCloser, password string) (io.ReadCloser, error) {
	if len(password) == 0 {
		return reader, nil
	}

	return decrypt(reader, []byte(password))
}

func decrypt(ciphertext io.ReadCloser, password []byte) (plaintext io.ReadCloser, err error) {
	unarmored, err := armor.Decode(ciphertext)
	if err != nil {
		return
	}

	firstTimeCalled := true
	var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
		if firstTimeCalled {
			firstTimeCalled = false
			return password, nil
		}
		// Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid).
		// For most (but not all) cases, inputting a wrong passwords is expected to trigger this error.
		return nil, errors.New("gopenpgp: wrong password in symmetric decryption")
	}

	config := &packet.Config{
		DefaultCipher: packet.CipherAES256,
	}

	var emptyKeyRing openpgp.EntityList
	md, err := openpgp.ReadMessage(unarmored.Body, emptyKeyRing, prompt, config)
	if err != nil {
		// Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure
		return
	}

	plaintext = io.NopCloser(md.UnverifiedBody)

	return
}

type encryptWrapperReader struct {
	plaintext         io.Reader
	encrypt           io.WriteCloser
	armored           io.WriteCloser
	buffer            io.ReadWriter
	plaintextReadZero bool
}

func (e *encryptWrapperReader) Read(p []byte) (n int, err error) {
	p2 := make([]byte, len(p))

	n, _ = e.plaintext.Read(p2)
	if n == 0 {
		if !e.plaintextReadZero {
			err = e.encrypt.Close()
			if err != nil {
				return
			}

			err = e.armored.Close()
			if err != nil {
				return
			}

			e.plaintextReadZero = true
		}

		return e.buffer.Read(p)
	}

	return e.buffer.Read(p)
}

func (e *encryptWrapperReader) Close() error {
	return nil
}

func NewEncryptWrapperReader(plaintext io.Reader, armored, encrypt io.WriteCloser, buffer io.ReadWriter) io.ReadCloser {
	return &encryptWrapperReader{
		plaintext: io.TeeReader(plaintext, encrypt),
		encrypt:   encrypt,
		armored:   armored,
		buffer:    buffer,
	}
}

func encrypt(plaintext io.ReadCloser, password []byte) (ciphertext io.ReadCloser, err error) {
	bufferReadWriter := new(bytes.Buffer)
	armored, err := armor.Encode(bufferReadWriter, constants.PGPMessageHeader, nil)
	if err != nil {
		return
	}
	config := &packet.Config{
		DefaultCipher: packet.CipherAES256,
		Time:          time.Now,
	}

	hints := &openpgp.FileHints{
		IsBinary: true,
		FileName: "",
		ModTime:  time.Unix(time.Now().Unix(), 0),
	}

	encryptWriter, err := openpgp.SymmetricallyEncrypt(armored, password, hints, config)
	if err != nil {
		return
	}

	ciphertext = NewEncryptWrapperReader(plaintext, armored, encryptWriter, bufferReadWriter)

	return
}

func healthHandler(w http.ResponseWriter, _ *http.Request) {
	_, _ = w.Write([]byte("Approaching Neutral Zone, all systems normal and functioning."))
}

func canContainsXSS(contentType string) bool {
	switch {
	case strings.Contains(contentType, "cache-manifest"):
		fallthrough
	case strings.Contains(contentType, "html"):
		fallthrough
	case strings.Contains(contentType, "rdf"):
		fallthrough
	case strings.Contains(contentType, "vtt"):
		fallthrough
	case strings.Contains(contentType, "xml"):
		fallthrough
	case strings.Contains(contentType, "xsl"):
		return true
	case strings.Contains(contentType, "x-mixed-replace"):
		return true
	}

	return false
}

/* The preview handler will show a preview of the content for browsers (accept type text/html), and referer is not transfer.sh */
func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")

	vars := mux.Vars(r)

	token := vars["token"]
	filename := vars["filename"]

	metadata, err := s.checkMetadata(r.Context(), token, filename, false)

	if err != nil {
		s.logger.Printf("Error metadata: %s", err.Error())
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	contentType := metadata.ContentType
	contentLength, err := s.storage.Head(r.Context(), token, filename)
	if err != nil {
		http.Error(w, http.StatusText(404), 404)
		return
	}

	var templatePath string
	var content htmlTemplate.HTML

	switch {
	case strings.HasPrefix(contentType, "image/"):
		templatePath = "download.image.html"
	case strings.HasPrefix(contentType, "video/"):
		templatePath = "download.video.html"
	case strings.HasPrefix(contentType, "audio/"):
		templatePath = "download.audio.html"
	case strings.HasPrefix(contentType, "text/"):
		templatePath = "download.markdown.html"

		var reader io.ReadCloser
		if reader, _, err = s.storage.Get(r.Context(), token, filename, nil); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		var data []byte
		data = make([]byte, _5M)
		if _, err = reader.Read(data); err != io.EOF && err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		if strings.HasPrefix(contentType, "text/x-markdown") || strings.HasPrefix(contentType, "text/markdown") {
			unsafe := blackfriday.Run(data)
			output := bluemonday.UGCPolicy().SanitizeBytes(unsafe)
			content = htmlTemplate.HTML(output)
		} else if strings.HasPrefix(contentType, "text/plain") {
			content = htmlTemplate.HTML(fmt.Sprintf("<pre>%s</pre>", html.EscapeString(string(data))))
		} else {
			templatePath = "download.sandbox.html"
		}

	default:
		templatePath = "download.html"
	}

	relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename))
	resolvedURL := resolveURL(r, relativeURL, s.proxyPort)
	relativeURLGet, _ := url.Parse(path.Join(s.proxyPath, getPathPart, token, filename))
	resolvedURLGet := resolveURL(r, relativeURLGet, s.proxyPort)
	var png []byte
	png, err = qrcode.Encode(resolvedURL, qrcode.High, 150)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	qrCode := base64.StdEncoding.EncodeToString(png)

	hostname := getURL(r, s.proxyPort).Host
	webAddress := resolveWebAddress(r, s.proxyPath, s.proxyPort)

	data := struct {
		ContentType    string
		Content        htmlTemplate.HTML
		Filename       string
		URL            string
		URLGet         string
		URLRandomToken string
		Hostname       string
		WebAddress     string
		ContentLength  uint64
		GAKey          string
		UserVoiceKey   string
		QRCode         string
	}{
		contentType,
		content,
		filename,
		resolvedURL,
		resolvedURLGet,
		token,
		hostname,
		webAddress,
		contentLength,
		s.gaKey,
		s.userVoiceKey,
		qrCode,
	}

	if err := htmlTemplates.ExecuteTemplate(w, templatePath, data); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

}

// this handler will output html or text, depending on the
// support of the client (Accept header).

func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {
	// vars := mux.Vars(r)

	hostname := getURL(r, s.proxyPort).Host
	webAddress := resolveWebAddress(r, s.proxyPath, s.proxyPort)

	maxUploadSize := ""
	if s.maxUploadSize > 0 {
		maxUploadSize = formatSize(s.maxUploadSize)
	}

	purgeTime := ""
	if s.purgeDays > 0 {
		purgeTime = formatDurationDays(s.purgeDays)
	}

	data := struct {
		Hostname      string
		WebAddress    string
		EmailContact  string
		GAKey         string
		UserVoiceKey  string
		PurgeTime     string
		MaxUploadSize string
		SampleToken   string
		SampleToken2  string
	}{
		hostname,
		webAddress,
		s.emailContact,
		s.gaKey,
		s.userVoiceKey,
		purgeTime,
		maxUploadSize,
		token(s.randomTokenLength),
		token(s.randomTokenLength),
	}

	w.Header().Set("Vary", "Accept")
	if acceptsHTML(r.Header) {
		if err := htmlTemplates.ExecuteTemplate(w, "index.html", data); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	} else {
		if err := textTemplates.ExecuteTemplate(w, "index.txt", data); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
	}
}

func (s *Server) notFoundHandler(w http.ResponseWriter, _ *http.Request) {
	http.Error(w, http.StatusText(404), 404)
}

func sanitize(fileName string) string {
	t := transform.Chain(
		norm.NFD,
		runes.Remove(runes.In(unicode.Cc)),
		runes.Remove(runes.In(unicode.Cf)),
		runes.Remove(runes.In(unicode.Co)),
		runes.Remove(runes.In(unicode.Cs)),
		runes.Remove(runes.In(unicode.Other)),
		runes.Remove(runes.In(unicode.Zl)),
		runes.Remove(runes.In(unicode.Zp)),
		norm.NFC)
	newName, _, err := transform.String(t, fileName)
	if err != nil {
		return path.Base(fileName)
	}
	if len(newName) == 0 {
		newName = "_"
	}
	return path.Base(newName)
}

func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseMultipartForm(_24K); nil != err {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError)
		return
	}

	token := token(s.randomTokenLength)

	w.Header().Set("Content-Type", "text/plain")

	responseBody := ""

	for _, fHeaders := range r.MultipartForm.File {
		for _, fHeader := range fHeaders {
			filename := sanitize(fHeader.Filename)
			contentType := mime.TypeByExtension(filepath.Ext(fHeader.Filename))

			var f io.Reader
			var err error

			if f, err = fHeader.Open(); err != nil {
				s.logger.Printf("%s", err.Error())
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			file, err := os.CreateTemp(s.tempPath, "transfer-")
			defer s.cleanTmpFile(file)

			if err != nil {
				s.logger.Printf("%s", err.Error())
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			n, err := io.Copy(file, f)
			if err != nil {
				s.logger.Printf("%s", err.Error())
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			contentLength := n

			_, err = file.Seek(0, io.SeekStart)
			if err != nil {
				s.logger.Printf("%s", err.Error())
				return
			}

			if s.maxUploadSize > 0 && contentLength > s.maxUploadSize {
				s.logger.Print("Entity too large")
				http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
				return
			}

			if s.performClamavPrescan {
				status, err := s.performScan(file.Name())
				if err != nil {
					s.logger.Printf("%s", err.Error())
					http.Error(w, "Could not perform prescan", http.StatusInternalServerError)
					return
				}

				if status != clamavScanStatusOK {
					s.logger.Printf("prescan positive: %s", status)
					http.Error(w, "Clamav prescan found a virus", http.StatusPreconditionFailed)
					return
				}
			}

			metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)

			buffer := &bytes.Buffer{}
			if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
				s.logger.Printf("%s", err.Error())
				http.Error(w, "Could not encode metadata", http.StatusInternalServerError)

				return
			} else if err := s.storage.Put(r.Context(), token, fmt.Sprintf("%s.metadata", filename), buffer, "text/json", uint64(buffer.Len())); err != nil {
				s.logger.Printf("%s", err.Error())
				http.Error(w, "Could not save metadata", http.StatusInternalServerError)

				return
			}

			s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)

			reader, err := attachEncryptionReader(file, r.Header.Get("X-Encrypt-Password"))
			if err != nil {
				http.Error(w, "Could not crypt file", http.StatusInternalServerError)
				return
			}

			if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {
				s.logger.Printf("Backend storage error: %s", err.Error())
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return

			}

			filename = url.PathEscape(filename)
			relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename))
			deleteURL, _ := url.Parse(path.Join(s.proxyPath, token, filename, metadata.DeletionToken))
			w.Header().Add("X-Url-Delete", resolveURL(r, deleteURL, s.proxyPort))
			responseBody += fmt.Sprintln(getURL(r, s.proxyPort).ResolveReference(relativeURL).String())
		}
	}
	_, err := w.Write([]byte(responseBody))
	if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

func (s *Server) cleanTmpFile(f *os.File) {
	if f != nil {
		err := f.Close()
		if err != nil {
			s.logger.Printf("Error closing tmpfile: %s (%s)", err, f.Name())
		}

		err = os.Remove(f.Name())
		if err != nil {
			s.logger.Printf("Error removing tmpfile: %s (%s)", err, f.Name())
		}
	}
}

type metadata struct {
	// ContentType is the original uploading content type
	ContentType string
	// ContentLength is is the original uploading content length
	ContentLength int64
	// Downloads is the actual number of downloads
	Downloads int
	// MaxDownloads contains the maximum numbers of downloads
	MaxDownloads int
	// MaxDate contains the max age of the file
	MaxDate time.Time
	// DeletionToken contains the token to match against for deletion
	DeletionToken string
	// Encrypted contains if the file was encrypted
	Encrypted bool
	// DecryptedContentType is the original uploading content type
	DecryptedContentType string
}

func metadataForRequest(contentType string, contentLength int64, randomTokenLength int, r *http.Request) metadata {
	metadata := metadata{
		ContentType:   strings.ToLower(contentType),
		ContentLength: contentLength,
		MaxDate:       time.Time{},
		Downloads:     0,
		MaxDownloads:  -1,
		DeletionToken: token(randomTokenLength) + token(randomTokenLength),
	}

	if v := r.Header.Get("Max-Downloads"); v == "" {
	} else if v, err := strconv.Atoi(v); err != nil {
	} else {
		metadata.MaxDownloads = v
	}

	if v := r.Header.Get("Max-Days"); v == "" {
	} else if v, err := strconv.Atoi(v); err != nil {
	} else {
		metadata.MaxDate = time.Now().Add(time.Hour * 24 * time.Duration(v))
	}

	if password := r.Header.Get("X-Encrypt-Password"); password != "" {
		metadata.Encrypted = true
		metadata.ContentType = "text/plain; charset=utf-8"
		metadata.DecryptedContentType = contentType
	} else {
		metadata.Encrypted = false
	}

	return metadata
}

func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	filename := sanitize(vars["filename"])

	contentLength := r.ContentLength

	defer storage.CloseCheck(r.Body)

	reader := r.Body

	if contentLength < 1 || s.performClamavPrescan {
		file, err := os.CreateTemp(s.tempPath, "transfer-")
		defer s.cleanTmpFile(file)
		if err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}

		// queue file to disk, because s3 needs content length
		// and clamav prescan scans a file
		n, err := io.Copy(file, r.Body)
		if err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, err.Error(), http.StatusInternalServerError)

			return
		}

		_, err = file.Seek(0, io.SeekStart)
		if err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Cannot reset cache file", http.StatusInternalServerError)

			return
		}

		contentLength = n

		if s.performClamavPrescan {
			status, err := s.performScan(file.Name())
			if err != nil {
				s.logger.Printf("%s", err.Error())
				http.Error(w, "Could not perform prescan", http.StatusInternalServerError)
				return
			}

			if status != clamavScanStatusOK {
				s.logger.Printf("prescan positive: %s", status)
				http.Error(w, "Clamav prescan found a virus", http.StatusPreconditionFailed)
				return
			}
		}

		reader = file
	}

	if s.maxUploadSize > 0 && contentLength > s.maxUploadSize {
		s.logger.Print("Entity too large")
		http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge)
		return
	}

	if contentLength == 0 {
		s.logger.Print("Empty content-length")
		http.Error(w, "Could not upload empty file", http.StatusBadRequest)
		return
	}

	contentType := mime.TypeByExtension(filepath.Ext(vars["filename"]))

	token := token(s.randomTokenLength)

	metadata := metadataForRequest(contentType, contentLength, s.randomTokenLength, r)

	buffer := &bytes.Buffer{}
	if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Could not encode metadata", http.StatusInternalServerError)
		return
	} else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) {
		s.logger.Print("Invalid MaxDate")
		http.Error(w, "Invalid MaxDate, make sure Max-Days is smaller than 290 years", http.StatusBadRequest)
		return
	} else if err := s.storage.Put(r.Context(), token, fmt.Sprintf("%s.metadata", filename), buffer, "text/json", uint64(buffer.Len())); err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Could not save metadata", http.StatusInternalServerError)
		return
	}

	s.logger.Printf("Uploading %s %s %d %s", token, filename, contentLength, contentType)

	reader, err := attachEncryptionReader(reader, r.Header.Get("X-Encrypt-Password"))
	if err != nil {
		http.Error(w, "Could not crypt file", http.StatusInternalServerError)
		return
	}

	if err = s.storage.Put(r.Context(), token, filename, reader, contentType, uint64(contentLength)); err != nil {
		s.logger.Printf("Error putting new file: %s", err.Error())
		http.Error(w, "Could not save file", http.StatusInternalServerError)
		return
	}

	// w.Statuscode = 200

	w.Header().Set("Content-Type", "text/plain")

	filename = url.PathEscape(filename)
	relativeURL, _ := url.Parse(path.Join(s.proxyPath, token, filename))
	deleteURL, _ := url.Parse(path.Join(s.proxyPath, token, filename, metadata.DeletionToken))

	w.Header().Set("X-Url-Delete", resolveURL(r, deleteURL, s.proxyPort))

	_, _ = w.Write([]byte(resolveURL(r, relativeURL, s.proxyPort)))
}

func resolveURL(r *http.Request, u *url.URL, proxyPort string) string {
	r.URL.Path = ""

	return getURL(r, proxyPort).ResolveReference(u).String()
}

func resolveKey(key, proxyPath string) string {
	key = strings.TrimPrefix(key, "/")

	key = strings.TrimPrefix(key, proxyPath)

	key = strings.Replace(key, "\\", "/", -1)

	return key
}

func resolveWebAddress(r *http.Request, proxyPath string, proxyPort string) string {
	rUrl := getURL(r, proxyPort)

	var webAddress string

	if len(proxyPath) == 0 {
		webAddress = fmt.Sprintf("%s://%s/",
			rUrl.ResolveReference(rUrl).Scheme,
			rUrl.ResolveReference(rUrl).Host)
	} else {
		webAddress = fmt.Sprintf("%s://%s/%s",
			rUrl.ResolveReference(rUrl).Scheme,
			rUrl.ResolveReference(rUrl).Host,
			strings.TrimPrefix(proxyPath, "/"))
	}

	return webAddress
}

// Similar to the logic found here:
// https://github.com/golang/go/blob/release-branch.go1.14/src/net/http/clone.go#L22-L33
func cloneURL(u *url.URL) *url.URL {
	c := &url.URL{}
	*c = *u

	if u.User != nil {
		c.User = &url.Userinfo{}
		*c.User = *u.User
	}

	return c
}

func getURL(r *http.Request, proxyPort string) *url.URL {
	u := cloneURL(r.URL)

	if r.TLS != nil {
		u.Scheme = "https"
	} else if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
		u.Scheme = proto
	} else {
		u.Scheme = "http"
	}

	host, port, err := net.SplitHostPort(r.Host)
	if err != nil {
		host = r.Host
		port = ""
	}

	p := idna.New(idna.ValidateForRegistration())
	var hostFromPunycode string
	hostFromPunycode, err = p.ToUnicode(host)
	if err == nil {
		host = hostFromPunycode
	}

	if len(proxyPort) != 0 {
		port = proxyPort
	}

	if len(port) == 0 {
		u.Host = host
	} else {
		if port == "80" && u.Scheme == "http" {
			u.Host = host
		} else if port == "443" && u.Scheme == "https" {
			u.Host = host
		} else {
			u.Host = net.JoinHostPort(host, port)
		}
	}

	return u
}

func (metadata metadata) remainingLimitHeaderValues() (remainingDownloads, remainingDays string) {
	if metadata.MaxDate.IsZero() {
		remainingDays = "n/a"
	} else {
		timeDifference := time.Until(metadata.MaxDate)
		remainingDays = strconv.Itoa(int(timeDifference.Hours()/24) + 1)
	}

	if metadata.MaxDownloads == -1 {
		remainingDownloads = "n/a"
	} else {
		remainingDownloads = strconv.Itoa(metadata.MaxDownloads - metadata.Downloads)
	}

	return remainingDownloads, remainingDays
}

func (s *Server) lock(token, filename string) {
	key := path.Join(token, filename)

	lock, _ := s.locks.LoadOrStore(key, &sync.Mutex{})

	lock.(*sync.Mutex).Lock()
}

func (s *Server) unlock(token, filename string) {
	key := path.Join(token, filename)

	lock, _ := s.locks.LoadOrStore(key, &sync.Mutex{})

	lock.(*sync.Mutex).Unlock()
}

func (s *Server) checkMetadata(ctx context.Context, token, filename string, increaseDownload bool) (metadata, error) {
	s.lock(token, filename)
	defer s.unlock(token, filename)

	var metadata metadata

	r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename), nil)
	defer storage.CloseCheck(r)

	if err != nil {
		return metadata, err
	}

	if err := json.NewDecoder(r).Decode(&metadata); err != nil {
		return metadata, err
	} else if metadata.MaxDownloads != -1 && metadata.Downloads >= metadata.MaxDownloads {
		return metadata, errors.New("maxDownloads expired")
	} else if !metadata.MaxDate.IsZero() && time.Now().After(metadata.MaxDate) {
		return metadata, errors.New("maxDate expired")
	} else if metadata.MaxDownloads != -1 && increaseDownload {
		// todo(nl5887): mutex?

		// update number of downloads
		metadata.Downloads++

		buffer := &bytes.Buffer{}
		if err := json.NewEncoder(buffer).Encode(metadata); err != nil {
			return metadata, errors.New("could not encode metadata")
		} else if err := s.storage.Put(ctx, token, fmt.Sprintf("%s.metadata", filename), buffer, "text/json", uint64(buffer.Len())); err != nil {
			return metadata, errors.New("could not save metadata")
		}
	}

	return metadata, nil
}

func (s *Server) checkDeletionToken(ctx context.Context, deletionToken, token, filename string) error {
	s.lock(token, filename)
	defer s.unlock(token, filename)

	var metadata metadata

	r, _, err := s.storage.Get(ctx, token, fmt.Sprintf("%s.metadata", filename), nil)
	defer storage.CloseCheck(r)

	if s.storage.IsNotExist(err) {
		return errors.New("metadata doesn't exist")
	} else if err != nil {
		return err
	}

	if err := json.NewDecoder(r).Decode(&metadata); err != nil {
		return err
	} else if metadata.DeletionToken != deletionToken {
		return errors.New("deletion token doesn't match")
	}

	return nil
}

func (s *Server) purgeHandler() {
	ticker := time.NewTicker(s.purgeInterval)
	go func() {
		for {
			<-ticker.C
			err := s.storage.Purge(context.TODO(), s.purgeDays)
			if err != nil {
				s.logger.Printf("error cleaning up expired files: %v", err)
			}
		}
	}()
}

func (s *Server) deleteHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	token := vars["token"]
	filename := vars["filename"]
	deletionToken := vars["deletionToken"]

	if err := s.checkDeletionToken(r.Context(), deletionToken, token, filename); err != nil {
		s.logger.Printf("Error metadata: %s", err.Error())
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	err := s.storage.Delete(r.Context(), token, filename)
	if s.storage.IsNotExist(err) {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	} else if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Could not delete file.", http.StatusInternalServerError)
		return
	}
}

func (s *Server) zipHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	files := vars["files"]

	zipfilename := fmt.Sprintf("transfersh-%d.zip", uint16(time.Now().UnixNano()))

	w.Header().Set("Content-Type", "application/zip")
	commonHeader(w, zipfilename)

	zw := zip.NewWriter(w)

	for _, key := range strings.Split(files, ",") {
		key = resolveKey(key, s.proxyPath)

		token := strings.Split(key, "/")[0]
		filename := sanitize(strings.Split(key, "/")[1])

		if _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil {
			s.logger.Printf("Error metadata: %s", err.Error())
			continue
		}

		reader, _, err := s.storage.Get(r.Context(), token, filename, nil)
		defer storage.CloseCheck(reader)

		if err != nil {
			if s.storage.IsNotExist(err) {
				http.Error(w, "File not found", 404)
				return
			}

			s.logger.Printf("%s", err.Error())
			http.Error(w, "Could not retrieve file.", http.StatusInternalServerError)
			return
		}

		header := &zip.FileHeader{
			Name:   strings.Split(key, "/")[1],
			Method: zip.Store,

			Modified: time.Now().UTC(),
		}

		fw, err := zw.CreateHeader(header)

		if err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Internal server error.", http.StatusInternalServerError)
			return
		}

		if _, err = io.Copy(fw, reader); err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Internal server error.", http.StatusInternalServerError)
			return
		}
	}

	if err := zw.Close(); err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Internal server error.", http.StatusInternalServerError)
		return
	}
}

func (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	files := vars["files"]

	tarfilename := fmt.Sprintf("transfersh-%d.tar.gz", uint16(time.Now().UnixNano()))

	w.Header().Set("Content-Type", "application/x-gzip")
	commonHeader(w, tarfilename)

	gw := gzip.NewWriter(w)
	defer storage.CloseCheck(gw)

	zw := tar.NewWriter(gw)
	defer storage.CloseCheck(zw)

	for _, key := range strings.Split(files, ",") {
		key = resolveKey(key, s.proxyPath)

		token := strings.Split(key, "/")[0]
		filename := sanitize(strings.Split(key, "/")[1])

		if _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil {
			s.logger.Printf("Error metadata: %s", err.Error())
			continue
		}

		reader, contentLength, err := s.storage.Get(r.Context(), token, filename, nil)
		defer storage.CloseCheck(reader)

		if err != nil {
			if s.storage.IsNotExist(err) {
				http.Error(w, "File not found", 404)
				return
			}

			s.logger.Printf("%s", err.Error())
			http.Error(w, "Could not retrieve file.", http.StatusInternalServerError)
			return
		}

		header := &tar.Header{
			Name: strings.Split(key, "/")[1],
			Size: int64(contentLength),
		}

		err = zw.WriteHeader(header)
		if err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Internal server error.", http.StatusInternalServerError)
			return
		}

		if _, err = io.Copy(zw, reader); err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Internal server error.", http.StatusInternalServerError)
			return
		}
	}
}

func (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	files := vars["files"]

	tarfilename := fmt.Sprintf("transfersh-%d.tar", uint16(time.Now().UnixNano()))

	w.Header().Set("Content-Type", "application/x-tar")
	commonHeader(w, tarfilename)

	zw := tar.NewWriter(w)
	defer storage.CloseCheck(zw)

	for _, key := range strings.Split(files, ",") {
		key = resolveKey(key, s.proxyPath)

		token := strings.Split(key, "/")[0]
		filename := strings.Split(key, "/")[1]

		if _, err := s.checkMetadata(r.Context(), token, filename, true); err != nil {
			s.logger.Printf("Error metadata: %s", err.Error())
			continue
		}

		reader, contentLength, err := s.storage.Get(r.Context(), token, filename, nil)
		defer storage.CloseCheck(reader)

		if err != nil {
			if s.storage.IsNotExist(err) {
				http.Error(w, "File not found", 404)
				return
			}

			s.logger.Printf("%s", err.Error())
			http.Error(w, "Could not retrieve file.", http.StatusInternalServerError)
			return
		}

		header := &tar.Header{
			Name: strings.Split(key, "/")[1],
			Size: int64(contentLength),
		}

		err = zw.WriteHeader(header)
		if err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Internal server error.", http.StatusInternalServerError)
			return
		}

		if _, err = io.Copy(zw, reader); err != nil {
			s.logger.Printf("%s", err.Error())
			http.Error(w, "Internal server error.", http.StatusInternalServerError)
			return
		}
	}
}

func (s *Server) headHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	token := vars["token"]
	filename := vars["filename"]

	metadata, err := s.checkMetadata(r.Context(), token, filename, false)

	if err != nil {
		s.logger.Printf("Error metadata: %s", err.Error())
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	contentType := metadata.ContentType
	contentLength, err := s.storage.Head(r.Context(), token, filename)
	if s.storage.IsNotExist(err) {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	} else if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Could not retrieve file.", http.StatusInternalServerError)
		return
	}

	remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()

	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10))
	w.Header().Set("Connection", "close")
	w.Header().Set("X-Remaining-Downloads", remainingDownloads)
	w.Header().Set("X-Remaining-Days", remainingDays)
	w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")

	if s.storage.IsRangeSupported() {
		w.Header().Set("Accept-Ranges", "bytes")
	}
}

func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
	vars := mux.Vars(r)

	action := vars["action"]
	token := vars["token"]
	filename := vars["filename"]

	metadata, err := s.checkMetadata(r.Context(), token, filename, true)

	if err != nil {
		s.logger.Printf("Error metadata: %s", err.Error())
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	}

	var rng *storage.Range
	if r.Header.Get("Range") != "" {
		rng = storage.ParseRange(r.Header.Get("Range"))
	}

	contentType := metadata.ContentType
	reader, contentLength, err := s.storage.Get(r.Context(), token, filename, rng)
	defer storage.CloseCheck(reader)

	if s.storage.IsNotExist(err) {
		http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
		return
	} else if err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Could not retrieve file.", http.StatusInternalServerError)
		return
	}
	if rng != nil {
		cr := rng.ContentRange()
		if cr != "" {
			w.Header().Set("Accept-Ranges", "bytes")
			w.Header().Set("Content-Range", cr)
			if rng.Limit > 0 {
				reader = io.NopCloser(io.LimitReader(reader, int64(rng.Limit)))
			}
		}
	}

	var disposition string
	if action == "inline" {
		disposition = "inline"
		/*
			metadata.ContentType is unable to determine the type of the content,
			So add text/plain in this case to fix XSS related issues/
		*/
		if strings.TrimSpace(contentType) == "" {
			contentType = "text/plain; charset=utf-8"
		}
	} else {
		disposition = "attachment"
	}

	remainingDownloads, remainingDays := metadata.remainingLimitHeaderValues()

	w.Header().Set("Content-Disposition", fmt.Sprintf(`%s; filename="%s"`, disposition, filename))
	w.Header().Set("Connection", "keep-alive")
	w.Header().Set("Cache-Control", "no-store")
	w.Header().Set("X-Remaining-Downloads", remainingDownloads)
	w.Header().Set("X-Remaining-Days", remainingDays)

	password := r.Header.Get("X-Decrypt-Password")
	reader, err = attachDecryptionReader(reader, password)
	if err != nil {
		http.Error(w, "Could not decrypt file", http.StatusInternalServerError)
		return
	}

	if metadata.Encrypted && len(password) > 0 {
		contentType = metadata.DecryptedContentType
		contentLength = uint64(metadata.ContentLength)
	}

	w.Header().Set("Content-Type", contentType)
	w.Header().Set("Content-Length", strconv.FormatUint(contentLength, 10))
	w.Header().Set("Vary", "Range, Referer, X-Decrypt-Password")

	if rng != nil && rng.ContentRange() != "" {
		w.WriteHeader(http.StatusPartialContent)
	}

	if disposition == "inline" && canContainsXSS(contentType) {
		reader = io.NopCloser(bluemonday.UGCPolicy().SanitizeReader(reader))
	}

	if _, err = io.Copy(w, reader); err != nil {
		s.logger.Printf("%s", err.Error())
		http.Error(w, "Error occurred copying to output stream", http.StatusInternalServerError)
		return
	}
}

func commonHeader(w http.ResponseWriter, filename string) {
	w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
	w.Header().Set("Connection", "close")
	w.Header().Set("Cache-Control", "no-store")
}

// RedirectHandler handles redirect
func (s *Server) RedirectHandler(h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if !s.forceHTTPS {
			// we don't want to enforce https
		} else if r.URL.Path == "/health.html" {
			// health check url won't redirect
		} else if strings.HasSuffix(ipAddrFromRemoteAddr(r.Host), ".onion") {
			// .onion addresses cannot get a valid certificate, so don't redirect
		} else if r.Header.Get("X-Forwarded-Proto") == "https" {
		} else if r.TLS != nil {
		} else {
			u := getURL(r, s.proxyPort)
			u.Scheme = "https"
			if len(s.proxyPort) == 0 && len(s.TLSListenerString) > 0 {
				_, port, err := net.SplitHostPort(s.TLSListenerString)
				if err != nil || port == "443" {
					port = ""
				}

				if len(port) > 0 {
					u.Host = net.JoinHostPort(u.Hostname(), port)
				} else {
					u.Host = u.Hostname()
				}
			}

			http.Redirect(w, r, u.String(), http.StatusPermanentRedirect)
			return
		}

		h.ServeHTTP(w, r)
	}
}

// LoveHandler Create a log handler for every request it receives.
func LoveHandler(h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("x-made-with", "<3 by DutchCoders")
		w.Header().Set("x-served-by", "Proudly served by DutchCoders")
		w.Header().Set("server", "Transfer.sh HTTP Server")
		h.ServeHTTP(w, r)
	}
}

func ipFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if ipFilterOptions == nil {
			h.ServeHTTP(w, r)
		} else {
			WrapIPFilter(h, ipFilterOptions).ServeHTTP(w, r)
		}
	}
}

func (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if s.authUser == "" && s.authPass == "" && s.authHtpasswd == "" {
			h.ServeHTTP(w, r)
			return
		}

		if s.htpasswdFile == nil && s.authHtpasswd != "" {
			htpasswdFile, err := htpasswd.New(s.authHtpasswd, htpasswd.DefaultSystems, nil)
			if err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}

			s.htpasswdFile = htpasswdFile
		}

		if s.authIPFilter == nil && s.authIPFilterOptions != nil {
			s.authIPFilter = newIPFilter(s.authIPFilterOptions)
		}

		w.Header().Set("WWW-Authenticate", "Basic realm=\"Restricted\"")

		var authorized bool
		if s.authIPFilter != nil {
			remoteIP := realip.FromRequest(r)
			authorized = s.authIPFilter.Allowed(remoteIP)
		}

		username, password, authOK := r.BasicAuth()
		if !authOK && !authorized {
			http.Error(w, "Not authorized", http.StatusUnauthorized)
			return
		}

		if !authorized && username == s.authUser && password == s.authPass {
			authorized = true
		}

		if !authorized && s.htpasswdFile != nil {
			authorized = s.htpasswdFile.Match(username, password)
		}

		if !authorized {
			http.Error(w, "Not authorized", http.StatusUnauthorized)
			return
		}

		h.ServeHTTP(w, r)
	}
}


================================================
FILE: server/handlers_test.go
================================================
package server

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	"testing"

	. "gopkg.in/check.v1"
)

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }

var (
	_ = Suite(&suiteRedirectWithForceHTTPS{})
	_ = Suite(&suiteRedirectWithoutForceHTTPS{})
)

type suiteRedirectWithForceHTTPS struct {
	handler http.HandlerFunc
}

func (s *suiteRedirectWithForceHTTPS) SetUpTest(c *C) {
	srvr, err := New(ForceHTTPS())
	c.Assert(err, IsNil)

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = fmt.Fprintln(w, "Hello, client")
	})

	s.handler = srvr.RedirectHandler(handler)
}

func (s *suiteRedirectWithForceHTTPS) TestHTTPs(c *C) {
	req := httptest.NewRequest("GET", "https://test/test", nil)

	w := httptest.NewRecorder()
	s.handler(w, req)

	resp := w.Result()
	c.Assert(resp.StatusCode, Equals, http.StatusOK)
}

func (s *suiteRedirectWithForceHTTPS) TestOnion(c *C) {
	req := httptest.NewRequest("GET", "http://test.onion/test", nil)

	w := httptest.NewRecorder()
	s.handler(w, req)

	resp := w.Result()
	c.Assert(resp.StatusCode, Equals, http.StatusOK)
}

func (s *suiteRedirectWithForceHTTPS) TestXForwardedFor(c *C) {
	req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil)
	req.Header.Set("X-Forwarded-Proto", "https")

	w := httptest.NewRecorder()
	s.handler(w, req)

	resp := w.Result()
	c.Assert(resp.StatusCode, Equals, http.StatusOK)
}

func (s *suiteRedirectWithForceHTTPS) TestHTTP(c *C) {
	req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil)

	w := httptest.NewRecorder()
	s.handler(w, req)

	resp := w.Result()
	c.Assert(resp.StatusCode, Equals, http.StatusPermanentRedirect)
	c.Assert(resp.Header.Get("Location"), Equals, "https://127.0.0.1/test")
}

type suiteRedirectWithoutForceHTTPS struct {
	handler http.HandlerFunc
}

func (s *suiteRedirectWithoutForceHTTPS) SetUpTest(c *C) {
	srvr, err := New()
	c.Assert(err, IsNil)

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		_, _ = fmt.Fprintln(w, "Hello, client")
	})

	s.handler = srvr.RedirectHandler(handler)
}

func (s *suiteRedirectWithoutForceHTTPS) TestHTTP(c *C) {
	req := httptest.NewRequest("GET", "http://127.0.0.1/test", nil)

	w := httptest.NewRecorder()
	s.handler(w, req)

	resp := w.Result()
	c.Assert(resp.StatusCode, Equals, http.StatusOK)
}

func (s *suiteRedirectWithoutForceHTTPS) TestHTTPs(c *C) {
	req := httptest.NewRequest("GET", "https://127.0.0.1/test", nil)

	w := httptest.NewRecorder()
	s.handler(w, req)

	resp := w.Result()
	c.Assert(resp.StatusCode, Equals, http.StatusOK)
}


================================================
FILE: server/ip_filter.go
================================================
/*
MIT License
Copyright © 2016 <dev@jpillora.com>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

package server

import (
	"log"
	"net"
	"net/http"
	"os"
	"sync"

	"github.com/tomasen/realip"
)

// IPFilterOptions for ipFilter. Allowed takes precedence over Blocked.
// IPs can be IPv4 or IPv6 and can optionally contain subnet
// masks (/24). Note however, determining if a given IP is
// included in a subnet requires a linear scan so is less performant
// than looking up single IPs.
//
// This could be improved with some algorithmic magic.
type IPFilterOptions struct {
	//explicity allowed IPs
	AllowedIPs []string
	//explicity blocked IPs
	BlockedIPs []string
	//block by default (defaults to allow)
	BlockByDefault bool
	// TrustProxy enable check request IP from proxy
	TrustProxy bool

	Logger interface {
		Printf(format string, v ...interface{})
	}
}

// ipFilter
type ipFilter struct {
	//mut protects the below
	//rw since writes are rare
	mut            sync.RWMutex
	defaultAllowed bool
	ips            map[string]bool
	subnets        []*subnet
}

type subnet struct {
	str     string
	ipnet   *net.IPNet
	allowed bool
}

func newIPFilter(opts *IPFilterOptions) *ipFilter {
	if opts.Logger == nil {
		flags := log.LstdFlags
		opts.Logger = log.New(os.Stdout, "", flags)
	}
	f := &ipFilter{
		ips:            map[string]bool{},
		defaultAllowed: !opts.BlockByDefault,
	}
	for _, ip := range opts.BlockedIPs {
		f.BlockIP(ip)
	}
	for _, ip := range opts.AllowedIPs {
		f.AllowIP(ip)
	}
	return f
}

func (f *ipFilter) AllowIP(ip string) bool {
	return f.ToggleIP(ip, true)
}

func (f *ipFilter) BlockIP(ip string) bool {
	return f.ToggleIP(ip, false)
}

func (f *ipFilter) ToggleIP(str string, allowed bool) bool {
	//check if provided string describes a subnet
	if ip, network, err := net.ParseCIDR(str); err == nil {
		// containing only one ip?
		if n, total := network.Mask.Size(); n == total {
			f.mut.Lock()
			f.ips[ip.String()] = allowed
			f.mut.Unlock()
			return true
		}
		//check for existing
		f.mut.Lock()
		found := false
		for _, subnet := range f.subnets {
			if subnet.str == str {
				found = true
				subnet.allowed = allowed
				break
			}
		}
		if !found {
			f.subnets = append(f.subnets, &subnet{
				str:     str,
				ipnet:   network,
				allowed: allowed,
			})
		}
		f.mut.Unlock()
		return true
	}
	//check if plain ip
	if ip := net.ParseIP(str); ip != nil {
		f.mut.Lock()
		f.ips[ip.String()] = allowed
		f.mut.Unlock()
		return true
	}
	return false
}

// ToggleDefault alters the default setting
func (f *ipFilter) ToggleDefault(allowed bool) {
	f.mut.Lock()
	f.defaultAllowed = allowed
	f.mut.Unlock()
}

// Allowed returns if a given IP can pass through the filter
func (f *ipFilter) Allowed(ipstr string) bool {
	return f.NetAllowed(net.ParseIP(ipstr))
}

// NetAllowed returns if a given net.IP can pass through the filter
func (f *ipFilter) NetAllowed(ip net.IP) bool {
	//invalid ip
	if ip == nil {
		return false
	}
	//read lock entire function
	//except for db access
	f.mut.RLock()
	defer f.mut.RUnlock()
	//check single ips
	allowed, ok := f.ips[ip.String()]
	if ok {
		return allowed
	}
	//scan subnets for any allow/block
	blocked := false
	for _, subnet := range f.subnets {
		if subnet.ipnet.Contains(ip) {
			if subnet.allowed {
				return true
			}
			blocked = true
		}
	}
	if blocked {
		return false
	}

	//use default setting
	return f.defaultAllowed
}

// Blocked returns if a given IP can NOT pass through the filter
func (f *ipFilter) Blocked(ip string) bool {
	return !f.Allowed(ip)
}

// NetBlocked returns if a given net.IP can NOT pass through the filter
func (f *ipFilter) NetBlocked(ip net.IP) bool {
	return !f.NetAllowed(ip)
}

// Wrap the provided handler with simple IP blocking middleware
// using this IP filter and its configuration
func (f *ipFilter) Wrap(next http.Handler) http.Handler {
	return &ipFilterMiddleware{ipFilter: f, next: next}
}

// WrapIPFilter is equivalent to newIPFilter(opts) then Wrap(next)
func WrapIPFilter(next http.Handler, opts *IPFilterOptions) http.Handler {
	return newIPFilter(opts).Wrap(next)
}

type ipFilterMiddleware struct {
	*ipFilter
	next http.Handler
}

func (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	remoteIP := realip.FromRequest(r)

	if !m.ipFilter.Allowed(remoteIP) {
		//show simple forbidden text
		http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
		return
	}

	//success!
	m.next.ServeHTTP(w, r)
}


================================================
FILE: server/server.go
================================================
/*
The MIT License (MIT)

Copyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

package server

import (
	"context"
	cryptoRand "crypto/rand"
	"crypto/tls"
	"encoding/binary"
	"errors"
	"log"
	"math/rand"
	"mime"
	"net/http"
	_ "net/http/pprof"
	"net/url"
	"os"
	"os/signal"
	"path/filepath"
	"strings"
	"sync"
	"syscall"
	"time"

	"github.com/PuerkitoBio/ghost/handlers"
	"github.com/VojtechVitek/ratelimit"
	"github.com/VojtechVitek/ratelimit/memory"
	gorillaHandlers "github.com/gorilla/handlers"
	"github.com/gorilla/mux"
	"github.com/tg123/go-htpasswd"
	"golang.org/x/crypto/acme/autocert"

	web "github.com/dutchcoders/transfer.sh-web"
	"github.com/dutchcoders/transfer.sh/server/storage"
	assetfs "github.com/elazarl/go-bindata-assetfs"
)

// parse request with maximum memory of _24Kilobits
const _24K = (1 << 3) * 24

// parse request with maximum memory of _5Megabytes
const _5M = (1 << 20) * 5

// OptionFn is the option function type
type OptionFn func(*Server)

// ClamavHost sets clamav host
func ClamavHost(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ClamAVDaemonHost = s
	}
}

// PerformClamavPrescan enables clamav prescan on upload
func PerformClamavPrescan(b bool) OptionFn {
	return func(srvr *Server) {
		srvr.performClamavPrescan = b
	}
}

// VirustotalKey sets virus total key
func VirustotalKey(s string) OptionFn {
	return func(srvr *Server) {
		srvr.VirusTotalKey = s
	}
}

// Listener set listener
func Listener(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ListenerString = s
	}

}

// CorsDomains sets CORS domains
func CorsDomains(s string) OptionFn {
	return func(srvr *Server) {
		srvr.CorsDomains = s
	}

}

// EmailContact sets email contact
func EmailContact(emailContact string) OptionFn {
	return func(srvr *Server) {
		srvr.emailContact = emailContact
	}
}

// GoogleAnalytics sets GA key
func GoogleAnalytics(gaKey string) OptionFn {
	return func(srvr *Server) {
		srvr.gaKey = gaKey
	}
}

// UserVoice sets UV key
func UserVoice(userVoiceKey string) OptionFn {
	return func(srvr *Server) {
		srvr.userVoiceKey = userVoiceKey
	}
}

// TLSListener sets TLS listener and option
func TLSListener(s string, t bool) OptionFn {
	return func(srvr *Server) {
		srvr.TLSListenerString = s
		srvr.TLSListenerOnly = t
	}

}

// ProfileListener sets profile listener
func ProfileListener(s string) OptionFn {
	return func(srvr *Server) {
		srvr.ProfileListenerString = s
	}
}

// WebPath sets web path
func WebPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = filepath.Join(s, "")
		}

		srvr.webPath = s
	}
}

// ProxyPath sets proxy path
func ProxyPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = filepath.Join(s, "")
		}

		srvr.proxyPath = s
	}
}

// ProxyPort sets proxy port
func ProxyPort(s string) OptionFn {
	return func(srvr *Server) {
		srvr.proxyPort = s
	}
}

// TempPath sets temp path
func TempPath(s string) OptionFn {
	return func(srvr *Server) {
		if s[len(s)-1:] != "/" {
			s = filepath.Join(s, "")
		}

		srvr.tempPath = s
	}
}

// LogFile sets log file
func LogFile(logger *log.Logger, s string) OptionFn {
	return func(srvr *Server) {
		f, err := os.OpenFile(s, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
		if err != nil {
			logger.Fatalf("error opening file: %v", err)
		}

		logger.SetOutput(f)
		srvr.logger = logger
	}
}

// Logger sets logger
func Logger(logger *log.Logger) OptionFn {
	return func(srvr *Server) {
		srvr.logger = logger
	}
}

// MaxUploadSize sets max upload size
func MaxUploadSize(kbytes int64) OptionFn {
	return func(srvr *Server) {
		srvr.maxUploadSize = kbytes * 1024
	}

}

// RateLimit set rate limit
func RateLimit(requests int) OptionFn {
	return func(srvr *Server) {
		srvr.rateLimitRequests = requests
	}
}

// RandomTokenLength sets random token length
func RandomTokenLength(length int) OptionFn {
	return func(srvr *Server) {
		srvr.randomTokenLength = length
	}
}

// Purge sets purge days and option
func Purge(days, interval int) OptionFn {
	return func(srvr *Server) {
		srvr.purgeDays = time.Duration(days) * time.Hour * 24
		srvr.purgeInterval = time.Duration(interval) * time.Hour
	}
}

// ForceHTTPS sets forcing https
func ForceHTTPS() OptionFn {
	return func(srvr *Server) {
		srvr.forceHTTPS = true
	}
}

// EnableProfiler sets enable profiler
func EnableProfiler() OptionFn {
	return func(srvr *Server) {
		srvr.profilerEnabled = true
	}
}

// UseStorage set storage to use
func UseStorage(s storage.Storage) OptionFn {
	return func(srvr *Server) {
		srvr.storage = s
	}
}

// UseLetsEncrypt set letsencrypt usage
func UseLetsEncrypt(hosts []string) OptionFn {
	return func(srvr *Server) {
		cacheDir := "./cache/"

		m := autocert.Manager{
			Prompt: autocert.AcceptTOS,
			Cache:  autocert.DirCache(cacheDir),
			HostPolicy: func(_ context.Context, host string) error {
				found := false

				for _, h := range hosts {
					found = found || strings.HasSuffix(host, h)
				}

				if !found {
					return errors.New("acme/autocert: host not configured")
				}

				return nil
			},
		}

		srvr.tlsConfig = m.TLSConfig()
		srvr.tlsConfig.GetCertificate = m.GetCertificate
	}
}

// TLSConfig sets TLS config
func TLSConfig(cert, pk string) OptionFn {
	certificate, err := tls.LoadX509KeyPair(cert, pk)
	return func(srvr *Server) {
		srvr.tlsConfig = &tls.Config{
			GetCertificate: func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
				return &certificate, err
			},
		}
	}
}

// HTTPAuthCredentials sets basic http auth credentials
func HTTPAuthCredentials(user string, pass string) OptionFn {
	return func(srvr *Server) {
		srvr.authUser = user
		srvr.authPass = pass
	}
}

// HTTPAuthHtpasswd sets basic http auth htpasswd file
func HTTPAuthHtpasswd(htpasswdPath string) OptionFn {
	return func(srvr *Server) {
		srvr.authHtpasswd = htpasswdPath
	}
}

// HTTPAUTHFilterOptions sets basic http auth ips whitelist
func HTTPAUTHFilterOptions(options IPFilterOptions) OptionFn {
	for i, allowedIP := range options.AllowedIPs {
		options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
	}

	return func(srvr *Server) {
		srvr.authIPFilterOptions = &options
	}
}

// FilterOptions sets ip filtering
func FilterOptions(options IPFilterOptions) OptionFn {
	for i, allowedIP := range options.AllowedIPs {
		options.AllowedIPs[i] = strings.TrimSpace(allowedIP)
	}

	for i, blockedIP := range options.BlockedIPs {
		options.BlockedIPs[i] = strings.TrimSpace(blockedIP)
	}

	return func(srvr *Server) {
		srvr.ipFilterOptions = &options
	}
}

// Server is the main application
type Server struct {
	authUser            string
	authPass            string
	authHtpasswd        string
	authIPFilterOptions *IPFilterOptions

	htpasswdFile *htpasswd.File
	authIPFilter *ipFilter

	logger *log.Logger

	tlsConfig *tls.Config

	profilerEnabled bool

	locks sync.Map

	maxUploadSize     int64
	rateLimitRequests int

	purgeDays     time.Duration
	purgeInterval time.Duration

	storage storage.Storage

	forceHTTPS bool

	randomTokenLength int

	ipFilterOptions *IPFilterOptions

	VirusTotalKey        string
	ClamAVDaemonHost     string
	performClamavPrescan bool

	tempPath string

	webPath      string
	proxyPath    string
	proxyPort    string
	emailContact string
	gaKey        string
	userVoiceKey string

	TLSListenerOnly bool

	CorsDomains           string
	ListenerString        string
	TLSListenerString     string
	ProfileListenerString string

	Certificate string

	LetsEncryptCache string
}

// New is the factory fot Server
func New(options ...OptionFn) (*Server, error) {
	s := &Server{
		locks: sync.Map{},
	}

	for _, optionFn := range options {
		optionFn(s)
	}

	return s, nil
}

var theRand *rand.Rand

func init() {
	var seedBytes [8]byte
	if _, err := cryptoRand.Read(seedBytes[:]); err != nil {
		panic("cannot obtain cryptographically secure seed")
	}

	theRand = rand.New(rand.NewSource(int64(binary.LittleEndian.Uint64(seedBytes[:]))))
}

// Run starts Server
func (s *Server) Run() {
	listening := false

	if s.profilerEnabled {
		listening = true

		go func() {
			s.logger.Println("Profiled listening at: :6060")

			_ = http.ListenAndServe(":6060", nil)
		}()
	}

	r := mux.NewRouter()

	var fs http.FileSystem

	if s.webPath != "" {
		s.logger.Println("Using static file path: ", s.webPath)

		fs = http.Dir(s.webPath)

		htmlTemplates, _ = htmlTemplates.ParseGlob(filepath.Join(s.webPath, "*.html"))
		textTemplates, _ = textTemplates.ParseGlob(filepath.Join(s.webPath, "*.txt"))
	} else {
		fs = &assetfs.AssetFS{
			Asset:    web.Asset,
			AssetDir: web.AssetDir,
			AssetInfo: func(path string) (os.FileInfo, error) {
				return os.Stat(path)
			},
			Prefix: web.Prefix,
		}

		for _, path := range web.AssetNames() {
			bytes, err := web.Asset(path)
			if err != nil {
				s.logger.Panicf("Unable to parse: path=%s, err=%s", path, err)
			}

			if strings.HasSuffix(path, ".html") {
				_, err = htmlTemplates.New(stripPrefix(path)).Parse(string(bytes))
				if err != nil {
					s.logger.Println("Unable to parse html template", err)
				}
			}
			if strings.HasSuffix(path, ".txt") {
				_, err = textTemplates.New(stripPrefix(path)).Parse(string(bytes))
				if err != nil {
					s.logger.Println("Unable to parse text template", err)
				}
			}
		}
	}

	staticHandler := http.FileServer(fs)

	r.PathPrefix("/images/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/styles/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/scripts/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/fonts/").Handler(staticHandler).Methods("GET")
	r.PathPrefix("/ico/").Handler(staticHandler).Methods("GET")
	r.HandleFunc("/favicon.ico", staticHandler.ServeHTTP).Methods("GET")
	r.HandleFunc("/robots.txt", staticHandler.ServeHTTP).Methods("GET")

	r.HandleFunc("/{filename:(?:favicon\\.ico|robots\\.txt|health\\.html)}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")

	r.HandleFunc("/health.html", healthHandler).Methods("GET")
	r.HandleFunc("/", s.viewHandler).Methods("GET")

	r.HandleFunc("/({files:.*}).zip", s.zipHandler).Methods("GET")
	r.HandleFunc("/({files:.*}).tar", s.tarHandler).Methods("GET")
	r.HandleFunc("/({files:.*}).tar.gz", s.tarGzHandler).Methods("GET")

	r.HandleFunc("/{token}/{filename}", s.headHandler).Methods("HEAD")
	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", s.headHandler).Methods("HEAD")

	r.HandleFunc("/{token}/{filename}", s.previewHandler).MatcherFunc(func(r *http.Request, rm *mux.RouteMatch) (match bool) {
		// The file will show a preview page when opening the link in browser directly or
		// from external link. If the referer url path and current path are the same it will be
		// downloaded.
		if !acceptsHTML(r.Header) {
			return false
		}

		match = r.Referer() == ""

		u, err := url.Parse(r.Referer())
		if err != nil {
			s.logger.Fatal(err)
			return
		}

		match = match || (u.Path != r.URL.Path)
		return
	}).Methods("GET")

	getHandlerFn := s.getHandler
	if s.rateLimitRequests > 0 {
		getHandlerFn = ratelimit.Request(ratelimit.IP).Rate(s.rateLimitRequests, 60*time.Second).LimitBy(memory.New())(http.HandlerFunc(getHandlerFn)).ServeHTTP
	}

	r.HandleFunc("/{token}/{filename}", getHandlerFn).Methods("GET")
	r.HandleFunc("/{action:(?:download|get|inline)}/{token}/{filename}", getHandlerFn).Methods("GET")

	r.HandleFunc("/{filename}/virustotal", s.virusTotalHandler).Methods("PUT")
	r.HandleFunc("/{filename}/scan", s.scanHandler).Methods("PUT")
	r.HandleFunc("/put/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/upload/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/{filename}", s.basicAuthHandler(http.HandlerFunc(s.putHandler))).Methods("PUT")
	r.HandleFunc("/", s.basicAuthHandler(http.HandlerFunc(s.postHandler))).Methods("POST")
	// r.HandleFunc("/{page}", viewHandler).Methods("GET")

	r.HandleFunc("/{token}/{filename}/{deletionToken}", s.deleteHandler).Methods("DELETE")

	r.NotFoundHandler = http.HandlerFunc(s.notFoundHandler)

	_ = mime.AddExtensionType(".md", "text/x-markdown")

	s.logger.Printf("Transfer.sh server started.\nusing temp folder: %s\nusing storage provider: %s", s.tempPath, s.storage.Type())

	var cors func(http.Handler) http.Handler
	if len(s.CorsDomains) > 0 {
		cors = gorillaHandlers.CORS(
			gorillaHandlers.AllowedHeaders([]string{"*"}),
			gorillaHandlers.AllowedOrigins(strings.Split(s.CorsDomains, ",")),
			gorillaHandlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"}),
		)
	} else {
		cors = func(h http.Handler) http.Handler {
			return h
		}
	}

	h := handlers.PanicHandler(
		ipFilterHandler(
			handlers.LogHandler(
				LoveHandler(
					s.RedirectHandler(cors(r))),
				handle
Download .txt
gitextract_9idmy_oq/

├── .bowerrc
├── .dockerignore
├── .github/
│   ├── build/
│   │   └── friendly-filenames.json
│   └── workflows/
│       ├── build-docker-images.yml
│       ├── release.yml
│       └── test.yml
├── .gitignore
├── .golangci.yml
├── .jshintrc
├── CODE_OF_CONDUCT.md
├── Dockerfile
├── LICENSE
├── Makefile
├── README.md
├── Vagrantfile
├── cmd/
│   └── cmd.go
├── examples.md
├── extras/
│   ├── clamd
│   └── transfersh
├── flake.nix
├── go.mod
├── go.sum
├── main.go
├── manifest.json
└── server/
    ├── clamav.go
    ├── handlers.go
    ├── handlers_test.go
    ├── ip_filter.go
    ├── server.go
    ├── storage/
    │   ├── common.go
    │   ├── gdrive.go
    │   ├── local.go
    │   ├── s3.go
    │   └── storj.go
    ├── token.go
    ├── token_test.go
    ├── utils.go
    └── virustotal.go
Download .txt
SYMBOL INDEX (186 symbols across 16 files)

FILE: cmd/cmd.go
  type Cmd (line 313) | type Cmd struct
  function versionCommand (line 317) | func versionCommand(_ *cli.Context) error {
  function New (line 323) | func New() *Cmd {

FILE: main.go
  function main (line 10) | func main() {

FILE: server/clamav.go
  constant clamavScanStatusOK (line 41) | clamavScanStatusOK = "OK"
  method scanHandler (line 43) | func (s *Server) scanHandler(w http.ResponseWriter, r *http.Request) {
  method performScan (line 78) | func (s *Server) performScan(path string) (string, error) {

FILE: server/handlers.go
  constant getPathPart (line 75) | getPathPart = "get"
  function stripPrefix (line 82) | func stripPrefix(path string) string {
  function initTextTemplates (line 86) | func initTextTemplates() *textTemplate.Template {
  function initHTMLTemplates (line 94) | func initHTMLTemplates() *htmlTemplate.Template {
  function attachEncryptionReader (line 103) | func attachEncryptionReader(reader io.ReadCloser, password string) (io.R...
  function attachDecryptionReader (line 111) | func attachDecryptionReader(reader io.ReadCloser, password string) (io.R...
  function decrypt (line 119) | func decrypt(ciphertext io.ReadCloser, password []byte) (plaintext io.Re...
  type encryptWrapperReader (line 152) | type encryptWrapperReader struct
    method Read (line 160) | func (e *encryptWrapperReader) Read(p []byte) (n int, err error) {
    method Close (line 185) | func (e *encryptWrapperReader) Close() error {
  function NewEncryptWrapperReader (line 189) | func NewEncryptWrapperReader(plaintext io.Reader, armored, encrypt io.Wr...
  function encrypt (line 198) | func encrypt(plaintext io.ReadCloser, password []byte) (ciphertext io.Re...
  function healthHandler (line 225) | func healthHandler(w http.ResponseWriter, _ *http.Request) {
  function canContainsXSS (line 229) | func canContainsXSS(contentType string) bool {
  method previewHandler (line 251) | func (s *Server) previewHandler(w http.ResponseWriter, r *http.Request) {
  method viewHandler (line 368) | func (s *Server) viewHandler(w http.ResponseWriter, r *http.Request) {
  method notFoundHandler (line 420) | func (s *Server) notFoundHandler(w http.ResponseWriter, _ *http.Request) {
  function sanitize (line 424) | func sanitize(fileName string) string {
  method postHandler (line 445) | func (s *Server) postHandler(w http.ResponseWriter, r *http.Request) {
  method cleanTmpFile (line 561) | func (s *Server) cleanTmpFile(f *os.File) {
  type metadata (line 575) | type metadata struct
    method remainingLimitHeaderValues (line 837) | func (metadata metadata) remainingLimitHeaderValues() (remainingDownlo...
  function metadataForRequest (line 594) | func metadataForRequest(contentType string, contentLength int64, randomT...
  method putHandler (line 627) | func (s *Server) putHandler(w http.ResponseWriter, r *http.Request) {
  function resolveURL (line 745) | func resolveURL(r *http.Request, u *url.URL, proxyPort string) string {
  function resolveKey (line 751) | func resolveKey(key, proxyPath string) string {
  function resolveWebAddress (line 761) | func resolveWebAddress(r *http.Request, proxyPath string, proxyPort stri...
  function cloneURL (line 782) | func cloneURL(u *url.URL) *url.URL {
  function getURL (line 794) | func getURL(r *http.Request, proxyPort string) *url.URL {
  method lock (line 854) | func (s *Server) lock(token, filename string) {
  method unlock (line 862) | func (s *Server) unlock(token, filename string) {
  method checkMetadata (line 870) | func (s *Server) checkMetadata(ctx context.Context, token, filename stri...
  method checkDeletionToken (line 906) | func (s *Server) checkDeletionToken(ctx context.Context, deletionToken, ...
  method purgeHandler (line 930) | func (s *Server) purgeHandler() {
  method deleteHandler (line 943) | func (s *Server) deleteHandler(w http.ResponseWriter, r *http.Request) {
  method zipHandler (line 967) | func (s *Server) zipHandler(w http.ResponseWriter, r *http.Request) {
  method tarGzHandler (line 1033) | func (s *Server) tarGzHandler(w http.ResponseWriter, r *http.Request) {
  method tarHandler (line 1094) | func (s *Server) tarHandler(w http.ResponseWriter, r *http.Request) {
  method headHandler (line 1152) | func (s *Server) headHandler(w http.ResponseWriter, r *http.Request) {
  method getHandler (line 1191) | func (s *Server) getHandler(w http.ResponseWriter, r *http.Request) {
  function commonHeader (line 1287) | func commonHeader(w http.ResponseWriter, filename string) {
  method RedirectHandler (line 1294) | func (s *Server) RedirectHandler(h http.Handler) http.HandlerFunc {
  function LoveHandler (line 1329) | func LoveHandler(h http.Handler) http.HandlerFunc {
  function ipFilterHandler (line 1338) | func ipFilterHandler(h http.Handler, ipFilterOptions *IPFilterOptions) h...
  method basicAuthHandler (line 1348) | func (s *Server) basicAuthHandler(h http.Handler) http.HandlerFunc {

FILE: server/handlers_test.go
  function Test (line 13) | func Test(t *testing.T) { TestingT(t) }
  type suiteRedirectWithForceHTTPS (line 20) | type suiteRedirectWithForceHTTPS struct
    method SetUpTest (line 24) | func (s *suiteRedirectWithForceHTTPS) SetUpTest(c *C) {
    method TestHTTPs (line 35) | func (s *suiteRedirectWithForceHTTPS) TestHTTPs(c *C) {
    method TestOnion (line 45) | func (s *suiteRedirectWithForceHTTPS) TestOnion(c *C) {
    method TestXForwardedFor (line 55) | func (s *suiteRedirectWithForceHTTPS) TestXForwardedFor(c *C) {
    method TestHTTP (line 66) | func (s *suiteRedirectWithForceHTTPS) TestHTTP(c *C) {
  type suiteRedirectWithoutForceHTTPS (line 77) | type suiteRedirectWithoutForceHTTPS struct
    method SetUpTest (line 81) | func (s *suiteRedirectWithoutForceHTTPS) SetUpTest(c *C) {
    method TestHTTP (line 92) | func (s *suiteRedirectWithoutForceHTTPS) TestHTTP(c *C) {
    method TestHTTPs (line 102) | func (s *suiteRedirectWithoutForceHTTPS) TestHTTPs(c *C) {

FILE: server/ip_filter.go
  type IPFilterOptions (line 31) | type IPFilterOptions struct
  type ipFilter (line 47) | type ipFilter struct
    method AllowIP (line 80) | func (f *ipFilter) AllowIP(ip string) bool {
    method BlockIP (line 84) | func (f *ipFilter) BlockIP(ip string) bool {
    method ToggleIP (line 88) | func (f *ipFilter) ToggleIP(str string, allowed bool) bool {
    method ToggleDefault (line 129) | func (f *ipFilter) ToggleDefault(allowed bool) {
    method Allowed (line 136) | func (f *ipFilter) Allowed(ipstr string) bool {
    method NetAllowed (line 141) | func (f *ipFilter) NetAllowed(ip net.IP) bool {
    method Blocked (line 174) | func (f *ipFilter) Blocked(ip string) bool {
    method NetBlocked (line 179) | func (f *ipFilter) NetBlocked(ip net.IP) bool {
    method Wrap (line 185) | func (f *ipFilter) Wrap(next http.Handler) http.Handler {
  type subnet (line 56) | type subnet struct
  function newIPFilter (line 62) | func newIPFilter(opts *IPFilterOptions) *ipFilter {
  function WrapIPFilter (line 190) | func WrapIPFilter(next http.Handler, opts *IPFilterOptions) http.Handler {
  type ipFilterMiddleware (line 194) | type ipFilterMiddleware struct
    method ServeHTTP (line 199) | func (m *ipFilterMiddleware) ServeHTTP(w http.ResponseWriter, r *http....

FILE: server/server.go
  constant _24K (line 61) | _24K = (1 << 3) * 24
  constant _5M (line 64) | _5M = (1 << 20) * 5
  type OptionFn (line 67) | type OptionFn
  function ClamavHost (line 70) | func ClamavHost(s string) OptionFn {
  function PerformClamavPrescan (line 77) | func PerformClamavPrescan(b bool) OptionFn {
  function VirustotalKey (line 84) | func VirustotalKey(s string) OptionFn {
  function Listener (line 91) | func Listener(s string) OptionFn {
  function CorsDomains (line 99) | func CorsDomains(s string) OptionFn {
  function EmailContact (line 107) | func EmailContact(emailContact string) OptionFn {
  function GoogleAnalytics (line 114) | func GoogleAnalytics(gaKey string) OptionFn {
  function UserVoice (line 121) | func UserVoice(userVoiceKey string) OptionFn {
  function TLSListener (line 128) | func TLSListener(s string, t bool) OptionFn {
  function ProfileListener (line 137) | func ProfileListener(s string) OptionFn {
  function WebPath (line 144) | func WebPath(s string) OptionFn {
  function ProxyPath (line 155) | func ProxyPath(s string) OptionFn {
  function ProxyPort (line 166) | func ProxyPort(s string) OptionFn {
  function TempPath (line 173) | func TempPath(s string) OptionFn {
  function LogFile (line 184) | func LogFile(logger *log.Logger, s string) OptionFn {
  function Logger (line 197) | func Logger(logger *log.Logger) OptionFn {
  function MaxUploadSize (line 204) | func MaxUploadSize(kbytes int64) OptionFn {
  function RateLimit (line 212) | func RateLimit(requests int) OptionFn {
  function RandomTokenLength (line 219) | func RandomTokenLength(length int) OptionFn {
  function Purge (line 226) | func Purge(days, interval int) OptionFn {
  function ForceHTTPS (line 234) | func ForceHTTPS() OptionFn {
  function EnableProfiler (line 241) | func EnableProfiler() OptionFn {
  function UseStorage (line 248) | func UseStorage(s storage.Storage) OptionFn {
  function UseLetsEncrypt (line 255) | func UseLetsEncrypt(hosts []string) OptionFn {
  function TLSConfig (line 283) | func TLSConfig(cert, pk string) OptionFn {
  function HTTPAuthCredentials (line 295) | func HTTPAuthCredentials(user string, pass string) OptionFn {
  function HTTPAuthHtpasswd (line 303) | func HTTPAuthHtpasswd(htpasswdPath string) OptionFn {
  function HTTPAUTHFilterOptions (line 310) | func HTTPAUTHFilterOptions(options IPFilterOptions) OptionFn {
  function FilterOptions (line 321) | func FilterOptions(options IPFilterOptions) OptionFn {
  type Server (line 336) | type Server struct
    method Run (line 417) | func (s *Server) Run() {
  function New (line 393) | func New(options ...OptionFn) (*Server, error) {
  function init (line 407) | func init() {

FILE: server/storage/common.go
  type Range (line 12) | type Range struct
    method Range (line 19) | func (r *Range) Range() string {
    method AcceptLength (line 29) | func (r *Range) AcceptLength(contentLength uint64) (newContentLength u...
    method SetContentRange (line 45) | func (r *Range) SetContentRange(cr string) {
    method ContentRange (line 50) | func (r *Range) ContentRange() string {
  function ParseRange (line 58) | func ParseRange(rng string) *Range {
  type Storage (line 92) | type Storage interface
  function CloseCheck (line 111) | func CloseCheck(c io.Closer) {

FILE: server/storage/gdrive.go
  type GDrive (line 23) | type GDrive struct
    method setupRoot (line 66) | func (s *GDrive) setupRoot() error {
    method hasChecksum (line 98) | func (s *GDrive) hasChecksum(f *drive.File) bool {
    method list (line 102) | func (s *GDrive) list(nextPageToken string, q string) (*drive.FileList...
    method findID (line 106) | func (s *GDrive) findID(filename string, token string) (string, error) {
    method Type (line 171) | func (s *GDrive) Type() string {
    method Head (line 176) | func (s *GDrive) Head(ctx context.Context, token string, filename stri...
    method Get (line 194) | func (s *GDrive) Get(ctx context.Context, token string, filename strin...
    method Delete (line 237) | func (s *GDrive) Delete(ctx context.Context, token string, filename st...
    method Purge (line 252) | func (s *GDrive) Purge(ctx context.Context, days time.Duration) (err e...
    method IsNotExist (line 284) | func (s *GDrive) IsNotExist(err error) bool {
    method Put (line 297) | func (s *GDrive) Put(ctx context.Context, token string, filename strin...
    method IsRangeSupported (line 334) | func (s *GDrive) IsRangeSupported() bool { return true }
  constant gDriveRootConfigFile (line 32) | gDriveRootConfigFile = "root_id.conf"
  constant gDriveTokenJSONFile (line 33) | gDriveTokenJSONFile = "token.json"
  constant gDriveDirectoryMimeType (line 34) | gDriveDirectoryMimeType = "application/vnd.google-apps.folder"
  function NewGDriveStorage (line 37) | func NewGDriveStorage(ctx context.Context, clientJSONFilepath string, lo...
  function getGDriveClient (line 337) | func getGDriveClient(ctx context.Context, config *oauth2.Config, localCo...
  function getGDriveTokenFromWeb (line 349) | func getGDriveTokenFromWeb(ctx context.Context, config *oauth2.Config, l...
  function gDriveTokenFromFile (line 367) | func gDriveTokenFromFile(file string) (*oauth2.Token, error) {
  function saveGDriveToken (line 379) | func saveGDriveToken(path string, token *oauth2.Token, logger *log.Logge...

FILE: server/storage/local.go
  type LocalStorage (line 14) | type LocalStorage struct
    method Type (line 26) | func (s *LocalStorage) Type() string {
    method Head (line 31) | func (s *LocalStorage) Head(_ context.Context, token string, filename ...
    method Get (line 45) | func (s *LocalStorage) Get(_ context.Context, token string, filename s...
    method Delete (line 73) | func (s *LocalStorage) Delete(_ context.Context, token string, filenam...
    method Purge (line 83) | func (s *LocalStorage) Purge(_ context.Context, days time.Duration) (e...
    method IsNotExist (line 105) | func (s *LocalStorage) IsNotExist(err error) bool {
    method Put (line 114) | func (s *LocalStorage) Put(_ context.Context, token string, filename s...
    method IsRangeSupported (line 138) | func (s *LocalStorage) IsRangeSupported() bool { return true }
  function NewLocalStorage (line 21) | func NewLocalStorage(basedir string, logger *log.Logger) (*LocalStorage,...

FILE: server/storage/s3.go
  type S3Storage (line 20) | type S3Storage struct
    method Type (line 54) | func (s *S3Storage) Type() string {
    method Head (line 59) | func (s *S3Storage) Head(ctx context.Context, token string, filename s...
    method Purge (line 79) | func (s *S3Storage) Purge(context.Context, time.Duration) (err error) {
    method IsNotExist (line 85) | func (s *S3Storage) IsNotExist(err error) bool {
    method Get (line 95) | func (s *S3Storage) Get(ctx context.Context, token string, filename st...
    method Delete (line 122) | func (s *S3Storage) Delete(ctx context.Context, token string, filename...
    method Put (line 146) | func (s *S3Storage) Put(ctx context.Context, token string, filename st...
    method IsRangeSupported (line 179) | func (s *S3Storage) IsRangeSupported() bool { return true }
  function NewS3Storage (line 30) | func NewS3Storage(ctx context.Context, accessKey, secretKey, bucketName ...
  function getAwsConfig (line 181) | func getAwsConfig(ctx context.Context, accessKey, secretKey string) (aws...

FILE: server/storage/storj.go
  type StorjStorage (line 16) | type StorjStorage struct
    method Type (line 60) | func (s *StorjStorage) Type() string {
    method Head (line 65) | func (s *StorjStorage) Head(ctx context.Context, token string, filenam...
    method Get (line 79) | func (s *StorjStorage) Get(ctx context.Context, token string, filename...
    method Delete (line 110) | func (s *StorjStorage) Delete(ctx context.Context, token string, filen...
    method Purge (line 121) | func (s *StorjStorage) Purge(context.Context, time.Duration) (err erro...
    method Put (line 127) | func (s *StorjStorage) Put(ctx context.Context, token string, filename...
    method IsRangeSupported (line 159) | func (s *StorjStorage) IsRangeSupported() bool { return true }
    method IsNotExist (line 162) | func (s *StorjStorage) IsNotExist(err error) bool {
  function NewStorjStorage (line 25) | func NewStorjStorage(ctx context.Context, access, bucket string, purgeDa...

FILE: server/token.go
  constant SYMBOLS (line 33) | SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  function token (line 37) | func token(length int) string {

FILE: server/token_test.go
  function BenchmarkTokenConcat (line 5) | func BenchmarkTokenConcat(b *testing.B) {
  function BenchmarkTokenLonger (line 11) | func BenchmarkTokenLonger(b *testing.B) {

FILE: server/utils.go
  function formatNumber (line 40) | func formatNumber(format string, s uint64) string {
  function renderFloat (line 70) | func renderFloat(format string, n float64) string {
  function ipAddrFromRemoteAddr (line 194) | func ipAddrFromRemoteAddr(s string) string {
  function acceptsHTML (line 202) | func acceptsHTML(hdr http.Header) bool {
  function formatSize (line 214) | func formatSize(size int64) string {
  function formatDurationDays (line 238) | func formatDurationDays(durationDays time.Duration) string {

FILE: server/virustotal.go
  method virusTotalHandler (line 36) | func (s *Server) virusTotalHandler(w http.ResponseWriter, r *http.Reques...
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (248K chars).
[
  {
    "path": ".bowerrc",
    "chars": 55,
    "preview": "{\n    \"directory\": \"transfersh-web/bower_components\"\n}\n"
  },
  {
    "path": ".dockerignore",
    "chars": 156,
    "preview": "build\npkg\ndist\nsrc\nbin\n*.pyc\n*.egg-info\n.vagrant\n.git\n.tmp\nbower_components\nnode_modules\nextras\nbuild\ntransfersh-server/"
  },
  {
    "path": ".github/build/friendly-filenames.json",
    "chars": 1813,
    "preview": "{\n    \"android-arm64\": { \"friendlyName\": \"android-arm64-v8a\" },\n    \"darwin-amd64\": { \"friendlyName\": \"darwin-amd64\" },\n"
  },
  {
    "path": ".github/workflows/build-docker-images.yml",
    "chars": 3635,
    "preview": "name: deploy multi-architecture Docker images for transfer.sh with buildx\n\non:\n  schedule:\n    - cron: '0 0 * * *' # eve"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 5704,
    "preview": "name: Build and Release\n\non:\n  workflow_dispatch:\n  release:\n    types: [published]\njobs:\n  build:\n    strategy:\n      m"
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 1593,
    "preview": "name: test\non:\n  pull_request:\n    branches:\n      - \"*\"\n  push:\n    branches:\n      - \"*\"\njobs:\n  test:\n    runs-on: ub"
  },
  {
    "path": ".gitignore",
    "chars": 271,
    "preview": "build/\npkg/\ndist/\nsrc/\nbin/\n*.pyc\n*.egg-info/\n.idea/\n\n.tmp\n.vagrant\n\nbower_components/\nnode_modules/\n\ntransfersh-server/"
  },
  {
    "path": ".golangci.yml",
    "chars": 297,
    "preview": "run:\n  deadline: 10m\n  issues-exit-code: 1\n  tests: true\n\noutput:\n  format: colored-line-number\n  print-issued-lines: tr"
  },
  {
    "path": ".jshintrc",
    "chars": 430,
    "preview": "{\n    \"node\": true,\n    \"browser\": true,\n    \"esnext\": true,\n    \"bitwise\": true,\n    \"camelcase\": true,\n    \"curly\": tr"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 1986,
    "preview": "\n# Contributor Code of Conduct\n\nAs contributors and maintainers of this project, and in the interest of fostering an ope"
  },
  {
    "path": "Dockerfile",
    "chars": 1366,
    "preview": "# Default to Go 1.24\nARG GO_VERSION=1.24\nFROM golang:${GO_VERSION}-alpine as build\n\n# Necessary to run 'go get' and to c"
  },
  {
    "path": "LICENSE",
    "chars": 1209,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2014-2018 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 And"
  },
  {
    "path": "Makefile",
    "chars": 92,
    "preview": ".PHONY: lint\n\nlint:\n\tgolangci-lint run --out-format=github-actions --config .golangci.yml \n\n"
  },
  {
    "path": "README.md",
    "chars": 24756,
    "preview": "# transfer.sh [![Go Report Card](https://goreportcard.com/badge/github.com/dutchcoders/transfer.sh)](https://goreportcar"
  },
  {
    "path": "Vagrantfile",
    "chars": 405,
    "preview": "# -*- mode: ruby -*-\n# vi: set ft=ruby :\n\n# Vagrantfile API/syntax version. Don't touch unless you know what you're doin"
  },
  {
    "path": "cmd/cmd.go",
    "chars": 14371,
    "preview": "package cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/dutchcoders/transfer.sh/server/storage\"\n\n\t\""
  },
  {
    "path": "examples.md",
    "chars": 10504,
    "preview": "# Table of Contents\n\n* [Aliases](#aliases)\n* [Uploading and downloading](#uploading-and-downloading)\n* [Archiving and ba"
  },
  {
    "path": "extras/clamd",
    "chars": 4308,
    "preview": "#! /bin/sh\n### BEGIN INIT INFO\n# Provides:          skeleton\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:   "
  },
  {
    "path": "extras/transfersh",
    "chars": 4579,
    "preview": "#! /bin/sh\n### BEGIN INIT INFO\n# Provides:          skeleton\n# Required-Start:    $remote_fs $syslog\n# Required-Stop:   "
  },
  {
    "path": "flake.nix",
    "chars": 12206,
    "preview": "{\n  description = \"Transfer.sh\";\n\n  inputs.flake-utils.url = \"github:numtide/flake-utils\";\n\n  outputs = { self, nixpkgs,"
  },
  {
    "path": "go.mod",
    "chars": 4776,
    "preview": "module github.com/dutchcoders/transfer.sh\n\ngo 1.22.0\n\nrequire (\n\tgithub.com/ProtonMail/go-crypto v0.0.0-20230217124315-7"
  },
  {
    "path": "go.sum",
    "chars": 38579,
    "preview": "cloud.google.com/go v0.16.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.26.0/go.mod h1"
  },
  {
    "path": "main.go",
    "chars": 178,
    "preview": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/dutchcoders/transfer.sh/cmd\"\n)\n\nfunc main() {\n\tapp := cmd.New()\n\terr :"
  },
  {
    "path": "manifest.json",
    "chars": 125,
    "preview": "{\n    \"dependencies\": {\n        \"github.com/dutchcoders/transfer.sh-web\": {\n            \"branch\": \"master\"\n        }\n   "
  },
  {
    "path": "server/clamav.go",
    "chars": 2867,
    "preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 "
  },
  {
    "path": "server/handlers.go",
    "chars": 38306,
    "preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 "
  },
  {
    "path": "server/handlers_test.go",
    "chars": 2591,
    "preview": "package server\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t. \"gopkg.in/check.v1\"\n)\n\n// Hook up gochec"
  },
  {
    "path": "server/ip_filter.go",
    "chars": 5474,
    "preview": "/*\nMIT License\nCopyright © 2016 <dev@jpillora.com>\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "server/server.go",
    "chars": 14895,
    "preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\n\nPermission is hereby gr"
  },
  {
    "path": "server/storage/common.go",
    "chars": 3046,
    "preview": "package storage\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype Range struct {\n\tStart        uint"
  },
  {
    "path": "server/storage/gdrive.go",
    "chars": 9133,
    "preview": "package storage\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n"
  },
  {
    "path": "server/storage/local.go",
    "chars": 3015,
    "preview": "package storage\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n)\n\n// LocalStorage is a local st"
  },
  {
    "path": "server/storage/s3.go",
    "chars": 4759,
    "preview": "package storage\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github"
  },
  {
    "path": "server/storage/storj.go",
    "chars": 4287,
    "preview": "package storage\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io\"\n\t\"log\"\n\t\"time\"\n\n\t\"storj.io/common/fpath\"\n\t\"storj.io/common/storj\"\n\t"
  },
  {
    "path": "server/token.go",
    "chars": 1513,
    "preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2020- Andrea Spacca and Stefan Benten.\n\nPermission is hereby granted, free of ch"
  },
  {
    "path": "server/token_test.go",
    "chars": 227,
    "preview": "package server\n\nimport \"testing\"\n\nfunc BenchmarkTokenConcat(b *testing.B) {\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = token(5) +"
  },
  {
    "path": "server/utils.go",
    "chars": 5992,
    "preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\nCopyright (c) 2018-2020 "
  },
  {
    "path": "server/virustotal.go",
    "chars": 1922,
    "preview": "/*\nThe MIT License (MIT)\n\nCopyright (c) 2014-2017 DutchCoders [https://github.com/dutchcoders/]\n\nPermission is hereby gr"
  }
]

About this extraction

This page contains the full source code of the dutchcoders/transfer.sh GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (226.0 KB), approximately 73.0k tokens, and a symbol index with 186 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!