Project not found: openSUSE:11.1

File pasta-0.7.2.obscpio of Package pasta

07070100000000000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001400000000pasta-0.7.2/.github07070100000001000081A400000000000000000000000164BFCD7C0000007A000000000000000000000000000000000000002300000000pasta-0.7.2/.github/dependabot.yml---

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "daily"
07070100000002000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001E00000000pasta-0.7.2/.github/workflows07070100000003000081A400000000000000000000000164BFCD7C000002F9000000000000000000000000000000000000002900000000pasta-0.7.2/.github/workflows/docker.yml---
name: docker image

'on':
  release:
    types: [published]

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      -
        name: Checkout
        uses: actions/checkout@v2
      -
        name: Set up QEMU
        uses: docker/setup-qemu-action@v2
      -
        name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v2
      -
        name: Login to DockerHub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      -
        name: Build and push
        uses: docker/build-push-action@v4
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: grisu48/pasta:latest
07070100000004000081A400000000000000000000000164BFCD7C0000047F000000000000000000000000000000000000002700000000pasta-0.7.2/.github/workflows/ghcr.yml---
# See https://docs.github.com/en/actions/publishing-packages/publishing-docker-images#publishing-images-to-github-packages

name: Create and publish container

'on':
  release:
    types: [published]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  github-image:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Log in to the Container registry
        uses: docker/login-action@v2
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata (tags, labels) for Docker
        id: meta
        uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}07070100000005000081A400000000000000000000000164BFCD7C000001BF000000000000000000000000000000000000002900000000pasta-0.7.2/.github/workflows/pastad.yml---

name: pastad

'on':
  push

jobs:
  pastad:
    name: pasta server
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
      - name: Setup go
        uses: actions/setup-go@v2
        with:
          go-version: '1.16'
      - name: Install requirements
        run: make requirements
      - name: Compile binaries
        run: make pastad pasta
      - name: Run tests
        run: make test
07070100000006000081A400000000000000000000000164BFCD7C000001A9000000000000000000000000000000000000001700000000pasta-0.7.2/.gitignore# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
/pastad
/pasta
cmd/pastad/pastad
cmd/pasta/pasta

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# vscode
.vscode
__debug_bin

# data files, directories and databases
*.db
bins
*.toml
pasta_test
/pastas
07070100000007000081A400000000000000000000000164BFCD7C00000133000000000000000000000000000000000000001A00000000pasta-0.7.2/ContainerfileFROM registry.suse.com/bci/golang AS build-env
WORKDIR /app
COPY . /app
RUN cd /app && make requirements && make pastad-static

FROM scratch
WORKDIR /data
COPY --from=build-env /app/pastad /app/mime.types /app/
ENTRYPOINT ["/app/pastad", "-m", "/app/mime.types", "-c", "/data/pastad.toml"]
VOLUME ["/data"]
070701000000080000A1FF00000000000000000000000164BFCD7C0000000D000000000000000000000000000000000000001700000000pasta-0.7.2/DockerfileContainerfile07070100000009000081A400000000000000000000000164BFCD7C00000433000000000000000000000000000000000000001400000000pasta-0.7.2/LICENSEMIT License

Copyright (c) 2020 Felix Niederwanger

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.
0707010000000A000081A400000000000000000000000164BFCD7C000002CB000000000000000000000000000000000000001500000000pasta-0.7.2/Makefiledefault: all
all: pasta pastad
static: pasta-static pastad-static

.PHONY: all test clean

requirements:
	go get github.com/BurntSushi/toml
	go get github.com/akamensky/argparse
	
pasta: cmd/pasta/*.go
	go build -o pasta $^
pastad: cmd/pastad/*.go
	go build -o pastad $^
pasta-static: cmd/pasta/*.go
	CGO_ENABLED=0 go build -ldflags="-w -s" -o pasta $^
pastad-static: cmd/pastad/*.go
	CGO_ENABLED=0 go build -ldflags="-w -s" -o pastad $^

test: pastad pasta
	go test ./...
	# TODO: This syntax is horrible :-)
	bash -c 'cd test && ./test.sh'

container-docker: Containerfile pasta pastad
	docker build . -t feldspaten.org/pasta

container-podman: Containerfile pasta pastad
	podman build . -t feldspaten.org/pasta

0707010000000B000081A400000000000000000000000164BFCD7C0000103E000000000000000000000000000000000000001600000000pasta-0.7.2/README.md![Build status badge](https://github.com/grisu48/pasta/workflows/pastad/badge.svg)

# pasta

Stupid simple pastebin service written in go.

The aim of this project is to create a simple pastebin service for self-hosting. pasta is self-contained, this means it does not need any additional services, e.g. a database to function. All it needs is a data directory and a config `toml` file and it will work.

This README contains the most important information. See the [docs](docs/index.md) folder for more documentation, e.g. the [getting-started](docs/getting-started.md) guide.

## Run as container (podman/docker)

The easiest way of self-hosting a `pasta` server is via the provided container from `ghcr.io/grisu48/pasta:latest`. Setup your own `pasta` instance is as easy as:

* Create your `data` directory (holds config + data)
* Create a [pastad.toml](pastad.toml.example) file therein
* Start the container, mount the `data` directory as `/data` and publish port `8199`
* Configure your reverse proxy (e.g. `nginx`) to forward requests to the `pasta` container

Assuming you want your data directory be e.g. `/srv/pasta`, prepare your server:

    mkdir /srv/pasta
    cp pastad.toml.example /srv/pastsa/pastad.toml
    $EDITOR /srv/pastsa/pastad.toml                     # Modify the configuration to your needs

And then create and run your container with your preferred container engine:

    docker container run -d --name pasta -v /srv/pasta:/data -p 127.0.0.1:8199:8199 ghcr.io/grisu48/pasta
    podman container run -d --name pasta -v /srv/pasta:/data -p 127.0.0.1:8199:8199 ghcr.io/grisu48/pasta

`pasta` listens here on port 8199 and all you need to do is to configure your reverse proxy (e.g. `nginx`) accordingly:

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name my-awesome-pasta.server;

    client_max_body_size 32M;
    location / {
        proxy_pass http://127.0.0.1:8199/;
    }
}
```
 
 Note that the good old [dockerhub image](https://hub.docker.com/r/grisu48/pasta/) is deprecated. It still gets updates but will be removed one fine day.

The container runs fine as rootless container (podman).

### environment variables

In addition to the config file, `pastad` can also be configured via environmental variables. This might be useful for running pasta as a container without a dedicated config file. Supported environmental variables are:

| Key | Description |
|-----|-------------|
| `PASTA_BASEURL` | Base URL for the pasta instance |
| `PASTA_PASTADIR` | Data directory for pastas |
| `PASTA_BINDADDR` | Address to bind the server to |
| `PASTA_MAXSIZE` | Maximum size (in Bytes) for new pastas |
| `PASTA_CHARACTERS` | Number of characters for new pastas |
| `PASTA_MIMEFILE` | MIME file |
| `PASTA_EXPIRE` | Default expiration time (in seconds) |
| `PASTA_CLEANUP` | Seconds between cleanup cycles |
| `PASTA_REQUESTDELAY` | Delay between requests from the same host in milliseconds |
| `PASTA_PUBLICPASTAS` | Number of public pastas to be displayed |

### macros

The `BASEURL` setting, defined either via configuration file or via the `PASTA_BASEURL` environment variable, supports custom macros, that should help you in various scenarios. Macros are pre-defined strings, which will be replaced.

The following macros are currently supported

| Macro | Replaced with | Example |
| `$hostname` | Local hostname | `localhost` |

A usage example would be to e.g. define the following in your local `pastad.conf`

```toml
BaseURL = "http://$hostname:8199"    # base URL as used within pasta
```

# Usage

Assuing the server runs on http://localhost:8199, you can use the `pasta` CLI tool (See below) or `curl`:

    curl -X POST 'http://localhost:8199' --data-binary @README.md

## pasta CLI

`pasta` is the CLI utility for making the creation of a pastas (i.e. files submitted to a pasta server) as easy as possible.  
For instance, if you want to push the `README.md` file and create a pasta out of it:

    pasta README.md
    pasta -r http://localhost:8199 REAME.md          # Define a custom remote server

`pasta` reads the config from `~/.pasta.toml` (see the [example file](pasta.toml.example))
0707010000000C000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001000000000pasta-0.7.2/cmd0707010000000D000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001600000000pasta-0.7.2/cmd/pasta0707010000000E000081A400000000000000000000000164BFCD7C000030B8000000000000000000000000000000000000001F00000000pasta-0.7.2/cmd/pasta/pasta.go/*
 * pasta client
 */
package main

import (
	"bufio"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/BurntSushi/toml"
)

const VERSION = "0.7.1"

type Config struct {
	RemoteHost  string       `toml:"RemoteHost"`
	RemoteHosts []RemoteHost `toml:"Remote"`
}
type RemoteHost struct {
	URL     string   `toml:"url"`     // URL of the remote host
	Alias   string   `toml:"alias"`   // Alias for the remote host
	Aliases []string `toml:"aliases"` // List of additional aliases for the remote host
}

var cf Config

// Search for the given remote alias. Returns true and the remote if found, otherwise false and an empty instance
func (cf *Config) FindRemoteAlias(remote string) (bool, RemoteHost) {
	for _, remote := range cf.RemoteHosts {
		if cf.RemoteHost == remote.Alias {
			return true, remote
		}
		for _, alias := range remote.Aliases {
			if cf.RemoteHost == alias {
				return true, remote
			}
		}
	}
	var ret RemoteHost
	return false, ret
}

/* http error instance */
type HttpError struct {
	err        string
	StatusCode int
}

func (e *HttpError) Error() string {
	return e.err
}

func FileExists(filename string) bool {
	_, err := os.Stat(filename)
	if err != nil {
		return false
	}
	return !os.IsNotExist(err)
}

func usage() {
	fmt.Printf("Usage: %s [OPTIONS] [FILE,[FILE2,...]]\n\n", os.Args[0])
	fmt.Println("OPTIONS")
	fmt.Println("     -h, --help                 Print this help message")
	fmt.Println("     -r, --remote HOST          Define remote host or alias (Default: http://localhost:8199)")
	fmt.Println("     -c, --config FILE          Define config file (Default: ~/.pasta.toml)")
	fmt.Println("     -f, --file FILE            Send FILE to server")
	fmt.Println("")
	fmt.Println("     --ls, --list               List known pasta pushes")
	fmt.Println("     --gc                       Garbage collector (clean expired pastas)")
	fmt.Println("     --version                  Show client version")
	fmt.Println("")
	fmt.Println("One or more files can be pushed to the server.")
	fmt.Println("If no file is given, the input from stdin will be pushed.")
}

func push(filename string, mime string, src io.Reader) (Pasta, error) {
	pasta := Pasta{}

	client := &http.Client{}
	// For compatability reasons, set the return format in URL and header for some time
	req, err := http.NewRequest("POST", cf.RemoteHost+"?ret=json", src)
	if err != nil {
		return pasta, err
	}
	req.Header.Set("Return-Format", "json")
	if mime != "" {
		req.Header.Set("Content-Type", mime)
	}
	if filename != "" {
		req.Header.Set("Filename", filename)
	}
	resp, err := client.Do(req)
	if err != nil {
		return pasta, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != 200 {
		return pasta, fmt.Errorf("http status code: %d", resp.StatusCode)
	}
	pasta.Date = time.Now().Unix()
	err = json.NewDecoder(resp.Body).Decode(&pasta)
	if err != nil {
		return pasta, err
	}
	return pasta, nil
}

func httpRequest(url string, method string) error {
	client := &http.Client{}
	req, err := http.NewRequest(method, url, nil)
	if err != nil {
		return err
	}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	if resp.StatusCode == 200 {
		return nil
	} else {
		// Try to fetch a small error message
		buf := make([]byte, 200)
		n, err := resp.Body.Read(buf)
		if err != nil || n == 0 || n >= 200 {
			return &HttpError{err: fmt.Sprintf("http code %d", resp.StatusCode), StatusCode: resp.StatusCode}
		}
		return &HttpError{err: fmt.Sprintf("http code %d: %s", resp.StatusCode, string(buf)), StatusCode: resp.StatusCode}
	}
}

func rm(pasta Pasta) error {
	url := fmt.Sprintf("%s?token=%s", pasta.Url, pasta.Token)
	if err := httpRequest(url, "DELETE"); err != nil {
		// Ignore 404 errors, because that means that the pasta is remove on the server (e.g. expired)
		if strings.HasPrefix(err.Error(), "http code 404") {
			return nil
		}
		return err
	}
	return nil
}

func getFilename(filename string) string {
	i := strings.LastIndex(filename, "/")
	if i < 0 {
		return filename
	} else {
		return filename[i+1:]
	}
}

/* Try to parse an integer range (1..2 or 5-9) - returns the range and a boolean indicating, if such a range could have been parsed */
func tryParseRange(txt string) (int, int, bool) {
	if txt == "" {
		return 0, 0, false
	}
	if i := strings.Index(txt, "-"); i > 0 {
		if i == 0 || i >= len(txt)-1 {
			// Incomplete range
			return 0, 0, false
		}
		l, r := txt[:i], txt[i+1:]
		// Try to parse
		i, err := strconv.Atoi(l)
		if err != nil {
			return 0, 0, false
		}
		j, err := strconv.Atoi(r)
		if err != nil {
			return 0, 0, false
		}
		return i, j, true
	}
	if i := strings.Index(txt, ".."); i > 1 {
		if i == 0 || i >= len(txt)-2 {
			// Incomplete range
			return 0, 0, false
		}
		l, r := txt[:i], txt[i+2:]
		// Try to parse
		i, err := strconv.Atoi(l)
		if err != nil {
			return 0, 0, false
		}
		j, err := strconv.Atoi(r)
		if err != nil {
			return 0, 0, false
		}
		return i, j, true
	}
	return 0, 0, false
}

func main() {
	cf.RemoteHost = "http://localhost:8199"
	action := ""
	// Load configuration file if possible
	homeDir, _ := os.UserHomeDir()
	configFile := homeDir + "/.pasta.toml"
	if FileExists(configFile) {
		if _, err := toml.DecodeFile(configFile, &cf); err != nil {
			fmt.Fprintf(os.Stderr, "config-toml file parse error: %s %s\n", configFile, err)
		}
	}
	// Files to be pushed
	files := make([]string, 0)
	explicit := false // marking files as explicitly given. This disabled the shortcut commands (ls, rm, gc)
	// Parse program arguments
	args := os.Args[1:]
	for i := 0; i < len(args); i++ {
		arg := args[i]
		if arg == "" {
			continue
		}
		if arg[0] == '-' {
			if arg == "-h" || arg == "--help" {
				usage()
				os.Exit(0)
			} else if arg == "-r" || arg == "--remote" {
				i++
				cf.RemoteHost = args[i]
			} else if arg == "-c" || arg == "--config" {
				i++
				if _, err := toml.DecodeFile(args[i], &cf); err != nil {
					fmt.Fprintf(os.Stderr, "config-toml file parse error: %s %s\n", configFile, err)
				}
			} else if arg == "-f" || arg == "--file" {
				i++
				explicit = true
				files = append(files, args[i])
			} else if arg == "--ls" || arg == "--list" {
				action = "list"
			} else if arg == "--rm" || arg == "--remote" || arg == "--delete" {
				action = "rm"
			} else if arg == "--gc" {
				action = "gc"
			} else if arg == "--version" {
				fmt.Printf("pasta version %s\n", VERSION)
				os.Exit(1)
			} else if arg == "--" {
				// The rest are filenames
				if i+1 < len(args) {
					files = append(files, args[i+1:]...)
				}
				i = len(args)
				continue
			} else {
				fmt.Fprintf(os.Stderr, "Invalid argument: %s\n", arg)
				os.Exit(1)
			}
		} else {
			files = append(files, arg)
		}
	}
	if found, remote := cf.FindRemoteAlias(cf.RemoteHost); found {
		fmt.Fprintf(os.Stderr, "Alias found: %s for %s\n", cf.RemoteHost, remote.URL)
		cf.RemoteHost = remote.URL
	}
	// Sanity checks
	if !strings.Contains(cf.RemoteHost, "://") {
		fmt.Fprintf(os.Stderr, "Invalid remote: %s\n", cf.RemoteHost)
		os.Exit(1)
	}
	// Load stored pastas
	stor, err := OpenStorage(homeDir + "/.pastas.dat")
	if err != nil {
		fmt.Fprintf(os.Stderr, "Cannot open pasta storage: %s\n", err)
	}

	if !explicit {
		// Special action: "pasta ls" list pasta
		if action == "" && len(files) == 1 && files[0] == "ls" {
			if FileExists(files[0]) {
				fmt.Fprintf(os.Stderr, "Ambiguous command %s (file '%s' exists) - please use '-f %s' to upload or --ls to list pastas\n", files[0], files[0], files[0])
				os.Exit(1)
			}
			action = "list"
			files = make([]string, 0)
		}
		// Special action: "pasta rm" is the same as "pasta --rm"
		if len(files) > 1 && files[0] == "rm" {
			if FileExists(files[0]) {
				fmt.Fprintf(os.Stderr, "Ambiguous command %s (file '%s' exists) - please use '-f %s' to upload or --rm to remove pastas\n", files[0], files[0], files[0])
				os.Exit(1)
			}
			action = "rm"
			files = files[1:]
		}
		// Special action: "pasta gc" is the same as "pasta --gc"
		if len(files) == 1 && (files[0] == "gc" || files[0] == "clean" || files[0] == "expire") {
			if FileExists(files[0]) {
				fmt.Fprintf(os.Stderr, "Ambiguous command %s (file '%s' exists) - please use '-f %s' to upload or --gc to cleanup expired pastas\n", files[0], files[0], files[0])
				os.Exit(1)
			}
			action = "gc"
			files = files[1:]
		}
	}

	if action == "push" || action == "" {
		if len(files) > 0 {
			for _, filename := range files {
				file, err := os.OpenFile(filename, os.O_RDONLY, 0400)
				if err != nil {
					fmt.Fprintf(os.Stderr, "%s: %s\n", filename, err)
					os.Exit(1)
				}
				defer file.Close()
				if stat, err := file.Stat(); err != nil {
					fmt.Fprintf(os.Stderr, "%s: %s\n", filename, err)
					os.Exit(1)
				} else if stat.Size() == 0 {
					fmt.Fprintf(os.Stderr, "Skipping empty file %s\n", filename)
					continue
				}
				// Push file
				f_name := getFilename(filename)
				pasta, err := push(f_name, "", file)
				pasta.Filename = f_name
				if err != nil {
					fmt.Fprintf(os.Stderr, "%s\n", err)
					os.Exit(1)
				}
				if err = stor.Append(pasta); err != nil {
					fmt.Fprintf(os.Stderr, "Cannot writing pasta to local store: %s\n", err)
				}
				// For a single file just print the link
				if len(files) == 1 {
					fmt.Printf("%s\n", pasta.Url)
				} else {
					fmt.Printf("%s - %s\n", pasta.Filename, pasta.Url)
				}
			}
		} else {
			fmt.Fprintln(os.Stderr, "Reading from stdin")
			reader := bufio.NewReader(os.Stdin)
			pasta, err := push("", "text/plain", reader)
			if err != nil {
				fmt.Fprintf(os.Stderr, "%s\n", err)
				os.Exit(1)
			}
			if err = stor.Append(pasta); err != nil {
				fmt.Fprintf(os.Stderr, "Cannot writing pasta to local store: %s\n", err)
			}
			fmt.Println(pasta.Url)
		}
	} else if action == "list" { // list known pastas
		if len(stor.Pastas) > 0 {
			fmt.Printf("Id   %-30s   %-19s   %s\n", "Filename", "Date", "URL")
			for i, pasta := range stor.Pastas {
				t := time.Unix(pasta.Date, 0)
				filename := pasta.Filename
				if filename == "" {
					filename = "<none>"
				}
				fmt.Printf("%-3d  %-30s   %-19s   %s\n", i, filename, t.Format("2006-01-02 15:04:05"), pasta.Url)
			}
		}
	} else if action == "rm" { // remove pastas
		// List of pastas to be deleted
		spoiled := make([]Pasta, 0)
		// Match given pastas and get tokens
		for _, file := range files {
			// If it is and integer, take the n-th item
			if id, err := strconv.Atoi(file); err == nil {
				if id < 0 || id >= len(stor.Pastas) {
					fmt.Fprintf(os.Stderr, "Cannot find pasta '%d'\n", id)
					os.Exit(1)
				}
				if id < 0 || id >= len(stor.Pastas) {
					fmt.Fprintf(os.Stderr, "Pasta index %d out of range\n", id)
					os.Exit(1)
				}
				spoiled = append(spoiled, stor.Pastas[id])
				// If it is a range (e.g. 3-4 or 3..4) use the i..j items
			} else if l, r, found := tryParseRange(file); found {
				// First ensure that the given string is not a file. Files have precedence
				if pasta, ok := stor.Get(file); ok {
					spoiled = append(spoiled, pasta)
				} else {
					// Assume it's a range
					if l < 0 {
						fmt.Fprintf(os.Stderr, "Pasta index %d out of range\n", l)
						os.Exit(1)
					}
					if r >= len(stor.Pastas) {
						fmt.Fprintf(os.Stderr, "Pasta index %d out of range\n", r)
						os.Exit(1)
					}
					for i := l; i <= r; i++ {
						spoiled = append(spoiled, stor.Pastas[i])
					}
				}
			} else {
				if pasta, ok := stor.Get(file); ok {
					spoiled = append(spoiled, pasta)
				} else {
					// Stop execution
					fmt.Fprintf(os.Stderr, "Cannot find pasta '%s'\n", file)
					os.Exit(1)
				}
			}
		}

		// Delete found pastas
		for _, pasta := range spoiled {
			if err := rm(pasta); err != nil {
				fmt.Fprintf(os.Stderr, "Error deleting '%s': %s\n", pasta.Url, err)
			} else {
				fmt.Printf("Deleted: %s\n", pasta.Url)
				stor.Remove(pasta.Url, pasta.Token) // Mark as removed for when rewriting storage
			}
		}
		// And re-write storage
		if err = stor.Write(); err != nil {
			fmt.Fprintf(os.Stderr, "Error writing to local storage: %s\n", err)
		}
	} else if action == "gc" || action == "clean" {
		// Cleanup happens when loading pastas
		expired := stor.ExpiredPastas()
		if expired == 0 {
			fmt.Println("all good")
		} else if expired == 1 {
			fmt.Println("one expired pasta cleared")
		} else {
			fmt.Printf("%d expired pastas cleared\n", expired)
		}
	} else {
		fmt.Fprintf(os.Stderr, "Unkown action: %s\n", action)
		os.Exit(1)
	}
}
0707010000000F000081A400000000000000000000000164BFCD7C00000ECF000000000000000000000000000000000000002100000000pasta-0.7.2/cmd/pasta/storage.gopackage main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
	"time"
)

type Pasta struct {
	Url      string `json:"url"`
	Token    string `json:"token"`
	Date     int64  `json:"date"`
	Expire   int64  `json:"expire"`
	Filename string `json:"filename"`
}

type Storage struct {
	Pastas   []Pasta
	file     *os.File
	filename string
	expired  int // number of expired pastas when loading
}

/* Format for writing to storage*/
func (pasta *Pasta) format() string {
	return fmt.Sprintf("%s:%d:%d:%s:%s", pasta.Token, pasta.Date, pasta.Expire, strings.Replace(pasta.Filename, ":", "", -1), pasta.Url)
}

func OpenStorage(filename string) (Storage, error) {
	stor := Storage{filename: filename}
	return stor, stor.Open(filename)
}

func (stor *Storage) Open(filename string) error {
	var err error
	stor.filename = filename
	stor.file, err = os.OpenFile(filename, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0640)
	if err != nil {
		return err
	}
	stor.Pastas = make([]Pasta, 0)
	dirty := false // dirty flag used to rewrite the file if some pastas are expired
	stor.expired = 0
	now := time.Now().Unix()
	// Read file
	scanner := bufio.NewScanner(stor.file)
	for scanner.Scan() {
		if err := scanner.Err(); err != nil {
			stor.file.Close()
			stor.file = nil
			return err
		}
		split := strings.Split(scanner.Text(), ":")
		if len(split) < 5 {
			continue
		}
		pasta := Pasta{Token: split[0], Filename: split[3], Url: strings.Join(split[4:], ":")}
		pasta.Date, _ = strconv.ParseInt(split[1], 10, 64)
		pasta.Expire, _ = strconv.ParseInt(split[2], 10, 64)
		// Don't add expired pastas and mark storage as dirty for re-write in the end
		if pasta.Expire != 0 && now > pasta.Expire {
			dirty = true
			stor.expired++
		} else {
			stor.Pastas = append(stor.Pastas, pasta)
		}
	}

	// Rewrite storage if expired pastas have been removed
	if dirty {
		return stor.Write()
	}
	return nil
}

func (stor *Storage) Close() error {
	if stor.file == nil {
		return nil
	}
	return stor.file.Close()
}

func (stor *Storage) Append(pasta Pasta) error {
	if _, err := stor.file.Write([]byte(pasta.format() + "\n")); err != nil {
		return err
	}
	return stor.file.Sync()
}

/* Rewrite the whole storage file */
func (stor *Storage) Write() error {
	var err error
	stor.file.Close()
	stor.file, err = os.OpenFile(stor.filename, os.O_RDWR|os.O_TRUNC, 0640)
	if err != nil {
		return err
	}
	for _, pasta := range stor.Pastas {
		if pasta.Url == "" {
			continue
		}
		_, err = stor.file.Write([]byte(pasta.format() + "\n"))
		if err != nil {
			return err
		}
	}
	return stor.file.Sync()
}

func (stor *Storage) ExpiredPastas() int {
	return stor.expired
}

func getPastaId(url string) string {
	i := strings.LastIndex(url, "/")
	if i < 0 {
		return url
	}
	return url[i+1:]
}

func (stor *Storage) Get(id string) (Pasta, bool) {
	// If the id is a url, check for url match first
	if strings.Contains(id, "://") {
		for _, pasta := range stor.Pastas {
			if pasta.Url == id {
				return pasta, true
			}
		}
	}
	// Check for pasta ID only. This needs to happen as second step als url matching has precedence
	for _, pasta := range stor.Pastas {
		if pasta.Url == id {
			return pasta, true
		}
	}

	// Nothing found, return empty pasta
	return Pasta{}, false
}

func (stor *Storage) find(url string, token string) int {
	for i, pasta := range stor.Pastas {
		if pasta.Url == url && pasta.Token == token {
			return i
		}
	}
	return -1
}

/** Marks the given pasta (given by url and token) as removed from storage. Returns true if the pasta is found, false if not found*/
func (stor *Storage) Remove(url string, token string) bool {
	i := stor.find(url, token)
	if i < 0 {
		return false
	}
	after := stor.Pastas[i+1:]
	stor.Pastas = stor.Pastas[:i]
	stor.Pastas = append(stor.Pastas, after...)
	return true

}
07070100000010000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001700000000pasta-0.7.2/cmd/pastad07070100000011000081A400000000000000000000000164BFCD7C00000FBE000000000000000000000000000000000000002100000000pasta-0.7.2/cmd/pastad/config.gopackage main

import (
	"fmt"
	"os"
)

type Config struct {
	BaseUrl         string `toml:"BaseURL"`  // Instance base URL
	PastaDir        string `toml:"PastaDir"` // dir where pasta are stored
	BindAddr        string `toml:"BindAddress"`
	MaxPastaSize    int64  `toml:"MaxPastaSize"` // Max bin size in bytes
	PastaCharacters int    `toml:"PastaCharacters"`
	MimeTypesFile   string `toml:"MimeTypes"`    // Load mime types from this file
	DefaultExpire   int64  `toml:"Expire"`       // Default expire time for a new pasta in seconds
	CleanupInterval int    `toml:"Cleanup"`      // Seconds between cleanup cycles
	RequestDelay    int64  `toml:"RequestDelay"` // Required delay between requests in milliseconds
	PublicPastas    int    `toml:"PublicPastas"` // Number of pastas to display on public page or 0 to disable
}

type ParserConfig struct {
	ConfigFile      *string
	BaseURL         *string
	PastaDir        *string
	BindAddr        *string
	MaxPastaSize    *int // parser doesn't support int64
	PastaCharacters *int
	MimeTypesFile   *string
	DefaultExpire   *int // parser doesn't support int64
	CleanupInterval *int
	PublicPastas    *int
}

func CreateDefaultConfigfile(filename string) error {
	hostname, _ := os.Hostname()
	if hostname == "" {
		hostname = "localhost"
	}
	content := []byte(fmt.Sprintf("BaseURL = 'http://%s:8199'\nBindAddress = ':8199'\nPastaDir = 'pastas'\nMaxPastaSize = 5242880       # 5 MiB\nPastaCharacters = 8\nExpire = 2592000             # 1 month\nCleanup = 3600               # cleanup interval in seconds\nRequestDelay = 2000\nPublicPastas = 0\n", hostname))
	file, err := os.Create(filename)
	if err != nil {
		return err
	}
	defer file.Close()
	if _, err = file.Write(content); err != nil {
		return err
	}
	if err := file.Chmod(0640); err != nil {
		return err
	}
	return file.Close()
}

// SetDefaults sets the default values to a config instance
func (cf *Config) SetDefaults() {
	cf.BaseUrl = "http://localhost:8199"
	cf.PastaDir = "pastas/"
	cf.BindAddr = "127.0.0.1:8199"
	cf.MaxPastaSize = 1024 * 1024 * 25 // Default max size: 25 MB
	cf.PastaCharacters = 8             // Note: Never use less than 8 characters!
	cf.MimeTypesFile = "mime.types"
	cf.DefaultExpire = 0
	cf.CleanupInterval = 60 * 60 // Default cleanup is once per hour
	cf.RequestDelay = 0          // By default not spam protection (Assume we are in safe environment)
	cf.PublicPastas = 0
}

// ReadEnv reads the environmental variables and sets the config accordingly
func (cf *Config) ReadEnv() {
	cf.BaseUrl = getenv("PASTA_BASEURL", cf.BaseUrl)
	cf.PastaDir = getenv("PASTA_PASTADIR", cf.PastaDir)
	cf.BindAddr = getenv("PASTA_BINDADDR", cf.BindAddr)
	cf.MaxPastaSize = getenv_i64("PASTA_MAXSIZE", cf.MaxPastaSize)
	cf.PastaCharacters = getenv_i("PASTA_CHARACTERS", cf.PastaCharacters)
	cf.MimeTypesFile = getenv("PASTA_MIMEFILE", cf.MimeTypesFile)
	cf.DefaultExpire = getenv_i64("PASTA_EXPIRE", cf.DefaultExpire)
	cf.CleanupInterval = getenv_i("PASTA_CLEANUP", cf.CleanupInterval)
	cf.RequestDelay = getenv_i64("PASTA_REQUESTDELAY", cf.RequestDelay)
	cf.PublicPastas = getenv_i("PASTA_PUBLICPASTAS", cf.PublicPastas)
}

func (pc *ParserConfig) ApplyTo(cf *Config) {
	if pc.BaseURL != nil && *pc.BaseURL != "" {
		cf.BaseUrl = *pc.BaseURL
	}
	if pc.PastaDir != nil && *pc.PastaDir != "" {
		cf.PastaDir = *pc.PastaDir
	}
	if pc.BindAddr != nil && *pc.BindAddr != "" {
		cf.BindAddr = *pc.BindAddr
	}
	if pc.MaxPastaSize != nil && *pc.MaxPastaSize > 0 {
		cf.MaxPastaSize = int64(*pc.MaxPastaSize)
	}
	if pc.PastaCharacters != nil && *pc.PastaCharacters > 0 {
		cf.PastaCharacters = *pc.PastaCharacters
	}
	if pc.MimeTypesFile != nil && *pc.MimeTypesFile != "" {
		cf.MimeTypesFile = *pc.MimeTypesFile
	}
	if pc.DefaultExpire != nil && *pc.DefaultExpire > 0 {
		cf.DefaultExpire = int64(*pc.DefaultExpire)
	}
	if pc.CleanupInterval != nil && *pc.CleanupInterval > 0 {
		cf.CleanupInterval = *pc.CleanupInterval
	}
	if pc.PublicPastas != nil && *pc.PublicPastas > 0 {
		cf.PublicPastas = *pc.PublicPastas
	}
}
07070100000012000081A400000000000000000000000164BFCD7C00005BA2000000000000000000000000000000000000002100000000pasta-0.7.2/cmd/pastad/pastad.go/*
 * pasted - stupid simple paste server
 */

package main

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/BurntSushi/toml"
	"github.com/akamensky/argparse"
)

const VERSION = "0.7"

var cf Config
var bowl PastaBowl
var publicPastas []Pasta
var mimeExtensions map[string]string
var delays map[string]int64
var delayMutex sync.Mutex

func SendPasta(pasta Pasta, w http.ResponseWriter) error {
	file, err := bowl.GetPastaReader(pasta.Id)
	if err != nil {
		return err
	}
	defer file.Close()
	w.Header().Set("Content-Disposition", "inline")
	w.Header().Set("Content-Length", strconv.FormatInt(pasta.Size, 10))
	if pasta.Mime != "" {
		w.Header().Set("Content-Type", pasta.Mime)
	}
	if pasta.ContentFilename != "" {
		w.Header().Set("Filename", pasta.ContentFilename)

	}
	_, err = io.Copy(w, file)
	return err
}

func removePublicPasta(id string) {
	copy := make([]Pasta, 0)
	for _, pasta := range publicPastas {
		if pasta.Id != id {
			copy = append(copy, pasta)
		}
	}
	publicPastas = copy
}

func deletePasta(id string, token string, w http.ResponseWriter) {
	var pasta Pasta
	var err error
	if id == "" || token == "" {
		goto Invalid
	}
	pasta, err = bowl.GetPasta(id)
	if err != nil {
		log.Fatalf("Error getting pasta %s: %s", pasta.Id, err)
		goto ServerError
	}
	if pasta.Id == "" {
		goto NotFound
	}
	if pasta.Token == token {
		err = bowl.DeletePasta(pasta.Id)
		if err != nil {
			log.Fatalf("Error deleting pasta %s: %s", pasta.Id, err)
			goto ServerError
		}
		// Also remove from public pastas, if present
		removePublicPasta(pasta.Id)

		w.WriteHeader(200)
		fmt.Fprintf(w, "<html><head><meta http-equiv=\"refresh\" content=\"2; url='%s'\" /></head>\n", cf.BaseUrl)
		fmt.Fprintf(w, "<body>\n")
		fmt.Fprintf(w, "<p>OK - Redirecting to <a href=\"/\">main page</a> ... </p>")
		fmt.Fprintf(w, "\n</body>\n</html>")
	} else {
		goto Invalid
	}
	return
NotFound:
	w.WriteHeader(404)
	fmt.Fprintf(w, "pasta not found")
	return
Invalid:
	w.WriteHeader(403)
	fmt.Fprintf(w, "Invalid request")
	return
ServerError:
	w.WriteHeader(500)
	fmt.Fprintf(w, "server error")
}

func receive(reader io.Reader, pasta *Pasta) error {
	buf := make([]byte, 4096)
	file, err := os.OpenFile(pasta.DiskFilename, os.O_RDWR|os.O_APPEND, 0640)
	if err != nil {
		file.Close()
		return err
	}
	defer file.Close()
	pasta.Size = 0
	for pasta.Size < cf.MaxPastaSize {
		n, err := reader.Read(buf)
		if (err == nil || err == io.EOF) && n > 0 {
			if _, err = file.Write(buf[:n]); err != nil {
				log.Fatalf("Write error while receiving bin: %s", err)
				return err
			}
			pasta.Size += int64(n)
		}
		if err != nil {
			if err == io.EOF {
				return nil
			}
			log.Fatalf("Receive error while receiving bin: %s", err)
			return err
		}
	}
	return nil
}

func receiveMultibody(r *http.Request, pasta *Pasta) (io.ReadCloser, bool, error) {
	public := false
	filename := ""

	// Read http headers first
	value := r.Header.Get("public")
	if value != "" {
		public = strBool(value, public)
	}
	// If the content length is given, reject immediately if the size is too big
	size := r.Header.Get("Content-Length")
	if size != "" {
		size, err := strconv.ParseInt(size, 10, 64)
		if err == nil && size > 0 && size > cf.MaxPastaSize {
			log.Println("Max size exceeded (Content-Length)")
			return nil, public, errors.New("content size exceeded")
		}
	}

	// Receive multipart form
	err := r.ParseMultipartForm(cf.MaxPastaSize)
	if err != nil {
		return nil, public, err
	}
	file, header, err := r.FormFile("file")
	if err != nil {
		return nil, public, err
	}

	// Read file headers
	filename = header.Filename
	if filename != "" {
		pasta.ContentFilename = filename
	}

	// Read form values after headers, as the form values have precedence
	form := r.MultipartForm
	values := form.Value
	if value, ok := values["public"]; ok {
		if len(value) > 0 {
			public = strBool(value[0], public)
		}
	}

	// Determine MIME type based on file extension, if present
	if filename != "" {
		pasta.Mime = mimeByFilename(filename)
	} else {
		pasta.Mime = "application/octet-stream"
	}

	return file, public, err
}

/* Parse expire header value. Returns expire value or 0 on error or invalid settings */
func parseExpire(headerValue []string) int64 {
	var ret int64
	for _, value := range headerValue {
		if expire, err := strconv.ParseInt(value, 10, 64); err == nil {
			// No negative values allowed
			if expire < 0 {
				return 0
			}
			ret = time.Now().Unix() + int64(expire)
		}
	}
	return ret
}

/* isMultipart returns true if the given request is multipart form */
func isMultipart(r *http.Request) bool {
	contentType := r.Header.Get("Content-Type")
	return contentType == "multipart/form-data" || strings.HasPrefix(contentType, "multipart/form-data;")
}

func ReceivePasta(r *http.Request) (Pasta, bool, error) {
	var err error
	var reader io.ReadCloser
	pasta := Pasta{Id: ""}
	public := false

	// Parse expire if given
	if cf.DefaultExpire > 0 {
		pasta.ExpireDate = time.Now().Unix() + cf.DefaultExpire
	}
	if expire := parseExpire(r.Header["Expire"]); expire > 0 {
		pasta.ExpireDate = expire
		// TODO: Add maximum expiration parameter
	}

	pasta.Id = removeNonAlphaNumeric(bowl.GenerateRandomBinId(cf.PastaCharacters))
	formRead := true // Read values from the form
	if isMultipart(r) {
		// InsertPasta to obtain a filename
		if err = bowl.InsertPasta(&pasta); err != nil {
			return pasta, public, err
		}
		reader, public, err = receiveMultibody(r, &pasta)
		if err != nil {
			bowl.DeletePasta(pasta.Id)
			pasta.Id = ""
			return pasta, public, err
		}
	} else {
		// Check if the input is coming from the POST form
		inputs := r.URL.Query()["input"]
		if len(inputs) > 0 && inputs[0] == "form" {
			// Copy reader, as r.FromValue consumes it's contents
			defer r.Body.Close()
			if content := r.FormValue("content"); content != "" {
				reader = io.NopCloser(strings.NewReader(content))
			} else {
				pasta.Id = "" // Empty pasta
				return pasta, public, nil
			}
		} else {
			reader = r.Body
			formRead = false
		}
	}
	defer reader.Close()

	header := r.Header
	// If the content length is given, reject immediately if the size is too big
	size := header.Get("Content-Length")
	if size != "" {
		size, err := strconv.ParseInt(size, 10, 64)
		if err == nil && size > 0 && size > cf.MaxPastaSize {
			log.Println("Max size exceeded (Content-Length)")
			return pasta, public, errors.New("content size exceeded")
		}
	}
	// Get property. URL parameter has precedence over header
	prop_get := func(name string) string {
		var val string
		if formRead {
			val = r.FormValue(name)
			if val != "" {
				return val
			}
		}
		val = header.Get(name)
		if val != "" {
			return val
		}
		return ""
	}
	// Check if public
	value := prop_get("public")
	if value != "" {
		public = strBool(value, public)
	}
	// Apply filename, if present
	// Due to inconsitent naming between URL and http parameters, we have to check for Filename and filename. URL parameters have precedence
	filename := prop_get("filename")
	if filename != "" {
		pasta.ContentFilename = filename
	} else {
		filename := prop_get("Filename")
		if filename != "" {
			pasta.ContentFilename = filename
		}
	}

	// InsertPasta sets filename
	if err = bowl.InsertPasta(&pasta); err != nil {
		return pasta, public, err
	}
	if err := receive(reader, &pasta); err != nil {
		return pasta, public, err
	}
	if pasta.Size >= cf.MaxPastaSize {
		log.Println("Max size exceeded while receiving bin")
		return pasta, public, errors.New("content size exceeded")
	}
	pasta.Mime = "text/plain"
	if pasta.Size == 0 {
		bowl.DeletePasta(pasta.Id)
		pasta.Id = ""
		pasta.DiskFilename = ""
		pasta.Token = ""
		pasta.ExpireDate = 0
		return pasta, public, nil
	}

	return pasta, public, nil
}

/* Delay a request for the given remote if required by spam protection */
func delayIfRequired(remote string) {
	if cf.RequestDelay == 0 {
		return
	}
	address := extractRemoteIP(remote)
	now := time.Now().UnixNano() / 1000000 // Timestamp now in milliseconds. This should be fine until 2262
	delayMutex.Lock()
	delay, ok := delays[address]
	delayMutex.Unlock()
	if ok {
		delta := cf.RequestDelay - (now - delay)
		if delta > 0 {
			time.Sleep(time.Duration(delta) * time.Millisecond)
		}
	}
	delays[address] = time.Now().UnixNano() / 1000000 // Fresh timestamp
}

func handlerHead(w http.ResponseWriter, r *http.Request) {
	var pasta Pasta
	id, err := ExtractPastaId(r.URL.Path)
	if err != nil {
		goto BadRequest
	}
	if pasta, err := bowl.GetPasta(id); err != nil {
		log.Fatalf("Error getting pasta %s: %s", pasta.Id, err)
		goto ServerError
	}
	if pasta.Id == "" {
		goto NotFound
	}

	w.Header().Set("Content-Length", strconv.FormatInt(pasta.Size, 10))
	if pasta.Mime != "" {
		w.Header().Set("Content-Type", pasta.Mime)
	}
	if pasta.ExpireDate > 0 {
		w.Header().Set("Expires", time.Unix(pasta.ExpireDate, 0).Format("2006-01-02-15:04:05"))
	}
	w.WriteHeader(200)
	fmt.Fprintf(w, "OK")
	return
ServerError:
	w.WriteHeader(500)
	fmt.Fprintf(w, "server error")
	return
NotFound:
	w.WriteHeader(404)
	fmt.Fprintf(w, "pasta not found")
	return
BadRequest:
	w.WriteHeader(400)
	if err == nil {
		fmt.Fprintf(w, "bad request")
	} else {
		fmt.Fprintf(w, "%s", err)
	}
	return
}

func handlerPost(w http.ResponseWriter, r *http.Request) {
	delayIfRequired(r.RemoteAddr)
	pasta, public, err := ReceivePasta(r)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		fmt.Fprintf(w, "server error")
		log.Printf("Receive error: %s", err)
		return
	} else {
		if pasta.Id == "" {
			w.WriteHeader(http.StatusBadRequest)
			w.Write([]byte("empty pasta"))
		} else {
			// Save into public pastas, if this is public
			if public {
				// Store at the beginning
				pastas := make([]Pasta, 1)
				pastas[0] = pasta
				pastas = append(pastas, publicPastas...)
				publicPastas = pastas
				// Crop to maximum allowed number
				if len(publicPastas) > cf.PublicPastas {
					publicPastas = publicPastas[len(publicPastas)-cf.PublicPastas:]
				}
				if err := bowl.WritePublicPastas(publicPastas); err != nil {
					log.Printf("Error writing public pastas: %s", err)
				}
			}

			log.Printf("Received pasta %s (%d bytes) from %s", pasta.Id, pasta.Size, r.RemoteAddr)
			w.WriteHeader(http.StatusOK)
			url := fmt.Sprintf("%s/%s", cf.BaseUrl, pasta.Id)
			// Return format. URL has precedence over http heder
			retFormat := r.Header.Get("Return-Format")
			retFormats := r.URL.Query()["ret"]
			if len(retFormats) > 0 {
				retFormat = retFormats[0]
			}
			if retFormat == "html" {
				// Website as return format
				fmt.Fprintf(w, "<!doctype html><html><head><title>pasta</title></head>\n")
				fmt.Fprintf(w, "<body>\n")
				fmt.Fprintf(w, "<h1>pasta</h1>\n")
				deleteLink := fmt.Sprintf("%s/delete?id=%s&token=%s", cf.BaseUrl, pasta.Id, pasta.Token)
				fmt.Fprintf(w, "<p>Pasta: <a href=\"%s\">%s</a> | <a href=\"%s\">🗑️ Delete</a><br/>", url, url, deleteLink)
				fmt.Fprintf(w, "<pre>")
				if pasta.ContentFilename != "" {
					fmt.Fprintf(w, "Filename:           %s\n", pasta.ContentFilename)
				}
				if pasta.Mime != "" {
					fmt.Fprintf(w, "Mime-Type:          %s\n", pasta.Mime)
				}
				if pasta.Size > 0 {
					fmt.Fprintf(w, "Size:               %d B\n", pasta.Size)
				}
				if pasta.ExpireDate > 0 {
					fmt.Fprintf(w, "Expiration:         %s\n", time.Unix(pasta.ExpireDate, 0).Format("2006-01-02-15:04:05"))
				}
				if public {
					fmt.Fprintf(w, "Public:             yes\n")
				}
				fmt.Fprintf(w, "Modification token: %s\n</pre>\n", pasta.Token)
				fmt.Fprintf(w, "<p>That was fun! Fancy <a href=\"%s\">another one?</a>.</p>\n", cf.BaseUrl)
				fmt.Fprintf(w, "</body></html>")
			} else if retFormat == "json" {
				// Dont use json package, the reply is simple enough to build it on-the-fly
				reply := fmt.Sprintf("{\"url\":\"%s\",\"token\":\"%s\", \"expire\":%d}", url, pasta.Token, pasta.ExpireDate)
				w.Write([]byte(reply))
			} else {
				fmt.Fprintf(w, "url:   %s\ntoken: %s\n", url, pasta.Token)
			}
		}
	}
}

func handler(w http.ResponseWriter, r *http.Request) {
	var err error
	if r.Method == http.MethodGet {
		// Check if bin ID is given
		id, err := ExtractPastaId(r.URL.Path)
		if err != nil {
			goto BadRequest
		}
		if id == "" {
			handlerIndex(w, r)
		} else {
			pasta, err := bowl.GetPasta(id)
			if err != nil {
				w.WriteHeader(http.StatusInternalServerError)
				fmt.Fprintf(w, "Storage error")
				log.Fatalf("Storage error: %s", err)
				return
			}
			if pasta.Id == "" {
				goto NoSuchPasta
			} else {
				// Delete expired pasta if present
				if pasta.Expired() {
					if err = bowl.DeletePasta(pasta.Id); err != nil {
						log.Fatalf("Cannot deleted expired pasta %s: %s", pasta.Id, err)
					}
					goto NoSuchPasta
				}

				if err = SendPasta(pasta, w); err != nil {
					log.Printf("Error sending pasta %s: %s", pasta.Id, err)
				}
			}
		}
	} else if r.Method == http.MethodPost || r.Method == http.MethodPut {
		handlerPost(w, r)
	} else if r.Method == http.MethodDelete {
		delayIfRequired(r.RemoteAddr)
		id, err := ExtractPastaId(r.URL.Path)
		if err != nil {
			goto BadRequest
		}
		token := takeFirst(r.URL.Query()["token"])
		deletePasta(id, token, w)
	} else if r.Method == http.MethodHead {
		handlerHead(w, r)
	} else {
		w.WriteHeader(http.StatusBadRequest)
		w.Write([]byte("Unsupported method"))
	}
	return
NoSuchPasta:
	w.WriteHeader(404)
	fmt.Fprintf(w, "No pasta\n\nSorry, there is no pasta for this link")
	return
BadRequest:
	w.WriteHeader(400)
	if err == nil {
		fmt.Fprintf(w, "bad request")
	} else {
		fmt.Fprintf(w, "%s", err)
	}
}

func handlerHealth(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "OK")
}
func handlerHealthJson(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "{\"status\":\"ok\"}")
}

func handlerPublic(w http.ResponseWriter, r *http.Request) {
	if cf.PublicPastas == 0 {
		w.WriteHeader(400)
		fmt.Fprintf(w, "public pasta listing is disabled")
		return
	}
	w.WriteHeader(200)
	w.Write([]byte("<html>\n<head>\n<title>public pastas</title>\n</head>\n<body>"))
	w.Write([]byte("<h2>public pastas</h2>\n"))
	w.Write([]byte("<table>\n"))
	w.Write([]byte("<tr><td>Filename</td><td>Size</td></tr>\n"))
	for _, pasta := range publicPastas {
		filename := pasta.ContentFilename
		if filename == "" {
			filename = pasta.Id
		}
		w.Write([]byte(fmt.Sprintf("<tr><td><a href=\"%s\">%s</a></td><td>%d B</td></tr>\n", pasta.Id, filename, pasta.Size)))
	}
	w.Write([]byte("</table>\n"))
	fmt.Fprintf(w, "<p>The server presents at most %d public pastas.<p>\n", cf.PublicPastas)
	w.Write([]byte("</body>\n"))
}

func handlerPublicJson(w http.ResponseWriter, r *http.Request) {
	if cf.PublicPastas == 0 {
		w.WriteHeader(400)
		fmt.Fprintf(w, "public pasta listing is disabled")
		return
	}
	type PublicPasta struct {
		Filename string `json:"filename"`
		Size     int64  `json:"size"`
		URL      string `json:"url"`
	}
	pastas := make([]PublicPasta, 0)
	for _, pasta := range publicPastas {
		filename := pasta.ContentFilename
		if filename == "" {
			filename = pasta.Id
		}
		pastas = append(pastas, PublicPasta{Filename: filename, URL: fmt.Sprintf("%s/%s", cf.BaseUrl, pasta.Id), Size: pasta.Size})
	}
	buf, err := json.Marshal(pastas)
	if err != nil {
		log.Printf("json error (public pastas): %s\n", err)
		goto ServerError
	}
	w.WriteHeader(200)
	w.Write(buf)
	return
ServerError:
	w.WriteHeader(500)
	w.Write([]byte("Server error"))
}

func handlerRobots(w http.ResponseWriter, r *http.Request) {
	// no robots allowed here
	fmt.Fprintf(w, "User-agent: *\nDisallow: /\n")
}

// Delete pasta
func handlerDelete(w http.ResponseWriter, r *http.Request) {
	delayIfRequired(r.RemoteAddr)
	id := takeFirst(r.URL.Query()["id"])
	token := takeFirst(r.URL.Query()["token"])
	deletePasta(id, token, w)
}

func handlerIndex(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "<!doctype html><html><head><title>pasta</title></head>\n")
	fmt.Fprintf(w, "<body>\n")
	fmt.Fprintf(w, "<h1>pasta</h1>\n")
	fmt.Fprintf(w, "<p>pasta is a stupid simple pastebin service for easy usage and deployment.</p>\n")
	// List public pastas, if enabled and available
	if cf.PublicPastas > 0 && len(publicPastas) > 0 {
		fmt.Fprintf(w, "<h2>Public pastas</h2>\n")
		fmt.Fprintf(w, "<table>\n")
		fmt.Fprintf(w, "<tr><td>Filename</td><td>Size</td></tr>\n")
		for _, pasta := range publicPastas {
			filename := pasta.ContentFilename
			if filename == "" {
				filename = pasta.Id
			}
			fmt.Fprintf(w, "<tr><td><a href=\"%s\">%s</a></td><td>%d B</td></tr>\n", pasta.Id, filename, pasta.Size)
		}
		fmt.Fprintf(w, "</table>\n")
		if len(publicPastas) == cf.PublicPastas {
			fmt.Fprintf(w, "<p>The server presents at most %d public pastas.<p>\n", cf.PublicPastas)
		}
	}
	fmt.Fprintf(w, "<h2>Post a new pasta</h2>\n")
	fmt.Fprintf(w, "<p><code>curl -X POST '%s' --data-binary @FILE</code></p>\n", cf.BaseUrl)
	if cf.DefaultExpire > 0 {
		fmt.Fprintf(w, "<p>pastas expire by default after %s - Enjoy them while they are fresh!</p>\n", timeHumanReadable(cf.DefaultExpire))
	}
	fmt.Fprintf(w, "<h3>File upload</h3>")
	fmt.Fprintf(w, "<p>Upload your file and make a fresh pasta out of it:</p>")
	fmt.Fprintf(w, "<form enctype=\"multipart/form-data\" method=\"post\" action=\"/?ret=html\">\n")
	fmt.Fprintf(w, "<input type=\"file\" name=\"file\">\n")
	if cf.PublicPastas > 0 {
		fmt.Fprintf(w, "<input type=\"checkbox\" id=\"public\" name=\"public\" value=\"true\"> Public\n")
	}
	fmt.Fprintf(w, "<input type=\"submit\" value=\"Upload\">\n")
	fmt.Fprintf(w, "</form>\n")
	fmt.Fprintf(w, "<h3>Text paste</h3>")
	fmt.Fprintf(w, "<p>Just paste your contents in the textfield and hit the <tt>pasta</tt> button below</p>\n")
	fmt.Fprintf(w, "<form method=\"post\" action=\"/?input=form&ret=html\">\n")
	fmt.Fprintf(w, "Filename (optional): <input type=\"text\" name=\"filename\" value=\"\" max=\"255\"><br/>\n")
	if cf.MaxPastaSize > 0 {
		fmt.Fprintf(w, "<textarea name=\"content\" rows=\"10\" cols=\"80\" maxlength=\"%d\"></textarea><br/>\n", cf.MaxPastaSize)
	} else {
		fmt.Fprintf(w, "<textarea name=\"content\" rows=\"10\" cols=\"80\"></textarea><br/>\n")
	}
	if cf.PublicPastas > 0 {
		fmt.Fprintf(w, "<input type=\"checkbox\" id=\"public\" name=\"public\" value=\"true\"> Public pasta\n")
	}
	fmt.Fprintf(w, "<input type=\"submit\" value=\"Pasta!\">\n")
	fmt.Fprintf(w, "</form>\n")
	fmt.Fprintf(w, "\n<hr/>\n")
	fmt.Fprintf(w, "<p>project page: <a href=\"https://codeberg.org/grisu48/pasta\" target=\"_BLANK\">codeberg.org/grisu48/pasta</a></p>\n")
	fmt.Fprintf(w, "</body></html>")
}

func cleanupThread() {
	// Double check this, because I know that I will screw this up at some point in the main routine :-)
	if cf.CleanupInterval == 0 {
		return
	}
	for {
		duration := time.Now().Unix()
		if err := bowl.RemoveExpired(); err != nil {
			log.Fatalf("Error while removing expired pastas: %s", err)
		}
		if cf.RequestDelay > 0 { // Cleanup of the spam protection addresses only if enabled
			delayMutex.Lock()
			delays = make(map[string]int64)
			delayMutex.Unlock()
		}
		duration = time.Now().Unix() - duration + int64(cf.CleanupInterval)
		if duration > 0 {
			time.Sleep(time.Duration(cf.CleanupInterval) * time.Second)
		} else {
			// Don't spam the system, give it at least some time
			time.Sleep(time.Second)
		}
	}
}

func main() {
	cf.SetDefaults()
	cf.ReadEnv()
	delays = make(map[string]int64)
	publicPastas = make([]Pasta, 0)
	// Parse program arguments for config
	parseCf := ParserConfig{}
	parser := argparse.NewParser("pastad", "pasta server")
	parseCf.ConfigFile = parser.String("c", "config", &argparse.Options{Default: "", Help: "Set config file"})
	parseCf.BaseURL = parser.String("B", "baseurl", &argparse.Options{Help: "Set base URL for instance"})
	parseCf.PastaDir = parser.String("d", "dir", &argparse.Options{Help: "Set pasta data directory"})
	parseCf.BindAddr = parser.String("b", "bind", &argparse.Options{Help: "Address to bind server to"})
	parseCf.MaxPastaSize = parser.Int("s", "size", &argparse.Options{Help: "Maximum allowed size for a pasta"})
	parseCf.PastaCharacters = parser.Int("n", "chars", &argparse.Options{Help: "Random characters for new pastas"})
	parseCf.MimeTypesFile = parser.String("m", "mime", &argparse.Options{Help: "Define mime types file"})
	parseCf.DefaultExpire = parser.Int("e", "expire", &argparse.Options{Help: "Pasta expire in seconds"})
	parseCf.CleanupInterval = parser.Int("C", "cleanup", &argparse.Options{Help: "Cleanup interval in seconds"})
	parseCf.PublicPastas = parser.Int("p", "public", &argparse.Options{Help: "Number of public pastas to display, if any"})
	if err := parser.Parse(os.Args); err != nil {
		fmt.Fprintf(os.Stderr, "%s\n", parser.Usage(err))
		os.Exit(1)
	}
	log.Printf("Starting pasta server v%s ... \n", VERSION)
	configFile := *parseCf.ConfigFile
	if configFile != "" {
		if FileExists(configFile) {
			if _, err := toml.DecodeFile(configFile, &cf); err != nil {
				fmt.Printf("Error loading configuration file: %s\n", err)
				os.Exit(1)
			}
		} else {
			if err := CreateDefaultConfigfile(configFile); err == nil {
				fmt.Fprintf(os.Stderr, "Created default config file '%s'\n", configFile)
			} else {
				fmt.Fprintf(os.Stderr, "Warning: Cannot create default config file '%s': %s\n", configFile, err)
			}
		}
	}
	// Program arguments overwrite config file
	parseCf.ApplyTo(&cf)

	// Sanity check
	if cf.PastaCharacters <= 0 {
		log.Println("Setting pasta characters to default 8 because it was <= 0")
		cf.PastaCharacters = 8
	}
	if cf.PastaCharacters < 8 {
		log.Println("Warning: Using less than 8 pasta characters might not be side-effects free")
	}
	if cf.PastaDir == "" {
		cf.PastaDir = "."
	}

	// Preparation steps
	baseURL, err := ApplyMacros(cf.BaseUrl)
	if err != nil {
		fmt.Fprintf(os.Stderr, "error applying macros: %s", err)
		os.Exit(1)
	}
	cf.BaseUrl = baseURL
	bowl.Directory = cf.PastaDir
	os.Mkdir(bowl.Directory, os.ModePerm)

	// Load MIME types file
	if cf.MimeTypesFile == "" {
		mimeExtensions = make(map[string]string, 0)
	} else {
		var err error
		mimeExtensions, err = loadMimeTypes(cf.MimeTypesFile)
		if err != nil {
			log.Printf("Warning: Cannot load mime types file '%s': %s", cf.MimeTypesFile, err)
		} else {
			log.Printf("Loaded %d mime types", len(mimeExtensions))
		}
	}

	// Load public pastas
	if cf.PublicPastas > 0 {
		pastas, err := bowl.GetPublicPastas()
		if err != nil {
			log.Printf("Error loading public pastas: %s", err)
		} else {
			// Crop if necessary
			if len(pastas) > cf.PublicPastas {
				pastas = pastas[len(pastas)-cf.PublicPastas:]
				bowl.WritePublicPastaIDs(pastas)
			}
			for _, id := range pastas {
				if id == "" {
					continue
				}
				pasta, err := bowl.GetPasta(id)
				if err == nil && pasta.Id != "" {
					publicPastas = append(publicPastas, pasta)
				}
			}
			log.Printf("Loaded %d public pastas", len(publicPastas))
		}
	}

	// Start cleanup thread
	if cf.CleanupInterval > 0 {
		go cleanupThread()
	}

	// Setup webserver
	http.HandleFunc("/", handler)
	http.HandleFunc("/health", handlerHealth)
	http.HandleFunc("/health.json", handlerHealthJson)
	http.HandleFunc("/public", handlerPublic)
	http.HandleFunc("/public.json", handlerPublicJson)
	http.HandleFunc("/delete", handlerDelete)
	http.HandleFunc("/robots.txt", handlerRobots)
	log.Printf("Serving http://%s", cf.BindAddr)
	log.Fatal(http.ListenAndServe(cf.BindAddr, nil))
}
07070100000013000081A400000000000000000000000164BFCD7C00001C92000000000000000000000000000000000000002200000000pasta-0.7.2/cmd/pastad/storage.gopackage main

import (
	"bufio"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strconv"
	"strings"
	"time"
)

type Pasta struct {
	Id              string // id of the pasta
	Token           string // modification token
	DiskFilename    string // filename for the pasta on the disk
	ContentFilename string // Filename of the content
	ExpireDate      int64  // Unix() date when it will expire
	Size            int64  // file size
	Mime            string // mime type
}

func (pasta *Pasta) Expired() bool {
	if pasta.ExpireDate == 0 {
		return false
	} else {
		return time.Now().Unix() > pasta.ExpireDate
	}
}

func randBytes(n int) []byte {
	buf := make([]byte, n)
	i, err := rand.Read(buf)
	if err != nil {
		panic(err)
	}
	if i < n {
		panic(fmt.Errorf("random generator empty"))
	}
	return buf
}

func randInt() int {
	buf := randBytes(4)
	ret := 0
	for i := 0; i < 4; i++ {
		ret += int(buf[i]) << (i * 8)
	}
	return ret
}

func RandomString(n int) string {
	var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	b := make([]rune, n)
	for i := range b {

		b[i] = letterRunes[randInt()%len(letterRunes)]
	}
	return string(b)
}

func FileExists(filename string) bool {
	_, err := os.Stat(filename)
	if err != nil {
		return false
	}
	return !os.IsNotExist(err)
}

func strBool(val string, def bool) bool {
	val = strings.TrimSpace(val)
	val = strings.ToLower(val)

	if val == "true" {
		return true
	} else if val == "yes" {
		return true
	} else if val == "on" {
		return true
	} else if val == "1" {
		return true
	} else if val == "false" {
		return false
	} else if val == "no" {
		return false
	} else if val == "off" {
		return false
	} else if val == "0" {
		return false
	}

	return def
}

/* PastaBowl is the main storage instance */
type PastaBowl struct {
	Directory string // Directory where the pastas are
}

func (bowl *PastaBowl) filename(id string) string {
	return fmt.Sprintf("%s/%s", bowl.Directory, id)
}

func (bowl *PastaBowl) Exists(id string) bool {
	return FileExists(bowl.filename(id))
}

/** Check for expired pastas and delete them */
func (bowl *PastaBowl) RemoveExpired() error {
	files, err := ioutil.ReadDir(bowl.Directory)
	if err != nil {
		return err
	}
	for _, file := range files {
		if file.Size() == 0 {
			continue
		}
		pasta, err := bowl.GetPasta(file.Name())
		if err != nil {
			return err
		}
		if pasta.Expired() {
			if err := bowl.DeletePasta(pasta.Id); err != nil {
				return err
			}
		}
	}
	return nil
}

// get pasta metadata
func (bowl *PastaBowl) GetPasta(id string) (Pasta, error) {
	pasta := Pasta{Id: "", DiskFilename: bowl.filename(id)}
	stat, err := os.Stat(bowl.filename(id))
	if err != nil {
		// Does not exists results in empty pasta result
		if !os.IsExist(err) {
			return pasta, nil
		}
		return pasta, err
	}
	pasta.Size = stat.Size()
	file, err := os.OpenFile(pasta.DiskFilename, os.O_RDONLY, 0400)
	if err != nil {
		return pasta, err
	}
	defer file.Close()
	// Read metadata (until "---")
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		if err = scanner.Err(); err != nil {
			return pasta, err
		}
		line := scanner.Text()
		pasta.Size -= int64(len(line) + 1)
		if line == "---" {
			break
		}
		// Parse metadata (name: value)
		i := strings.Index(line, ":")
		if i <= 0 {
			continue
		}
		name, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
		if name == "token" {
			pasta.Token = value
		} else if name == "expire" {
			pasta.ExpireDate, _ = strconv.ParseInt(value, 10, 64)
		} else if name == "mime" {
			pasta.Mime = value
		} else if name == "filename" {
			pasta.ContentFilename = value
		}

	}
	// All good
	pasta.Id = id
	return pasta, nil
}

func (bowl *PastaBowl) getPastaFile(id string, flag int) (*os.File, error) {
	filename := bowl.filename(id)
	file, err := os.OpenFile(filename, flag, 0640)
	if err != nil {
		return nil, err
	}
	buf := make([]byte, 1)
	c := 0 // Counter
	for {
		n, err := file.Read(buf)
		if err != nil {
			if err == io.EOF {
				file.Close()
				return nil, errors.New("unexpected end of block")
			}
			file.Close()
			return nil, errors.New("unexpected end of block")
		}
		if n == 0 {
			continue
		}
		if buf[0] == '-' {
			c++
		} else if buf[0] == '\n' {
			if c == 3 {
				return file, nil
			}
			c = 0
		} else {
			c = 0
		}
	}
}

// Get the file instance to the pasta content (read-only)
func (bowl *PastaBowl) GetPastaReader(id string) (*os.File, error) {
	return bowl.getPastaFile(id, os.O_RDONLY)
}

// Get the file instance to the pasta content (read-only)
func (bowl *PastaBowl) GetPastaWriter(id string) (*os.File, error) {
	return bowl.getPastaFile(id, os.O_RDWR)
}

// Prepare a pasta file to be written. Id and Token will be set, if not already done
func (bowl *PastaBowl) InsertPasta(pasta *Pasta) error {
	if pasta.Id == "" {
		// TODO: Use crypto rand
		pasta.Id = bowl.GenerateRandomBinId(8) // Use default length here
	}
	if pasta.Token == "" {
		// TODO: Use crypto rand
		pasta.Token = RandomString(16)
	}
	pasta.DiskFilename = bowl.filename(pasta.Id)
	file, err := os.OpenFile(pasta.DiskFilename, os.O_RDWR|os.O_CREATE, 0640)
	if err != nil {
		return err
	}
	defer file.Close()
	if _, err := file.Write([]byte(fmt.Sprintf("token:%s\n", pasta.Token))); err != nil {
		return err
	}
	if pasta.ExpireDate > 0 {
		if _, err := file.Write([]byte(fmt.Sprintf("expire:%d\n", pasta.ExpireDate))); err != nil {
			return err
		}
	}
	if pasta.Mime != "" {
		if _, err := file.Write([]byte(fmt.Sprintf("mime:%s\n", pasta.Mime))); err != nil {
			return err
		}
	}
	if pasta.ContentFilename != "" {
		if _, err := file.Write([]byte(fmt.Sprintf("filename:%s\n", pasta.ContentFilename))); err != nil {
			return err
		}
	}

	if _, err := file.Write([]byte("---\n")); err != nil {
		return err
	}
	return file.Sync()
}

func (bowl *PastaBowl) DeletePasta(id string) error {
	if !bowl.Exists(id) {
		return nil
	}
	return os.Remove(bowl.filename(id))
}

func (bowl *PastaBowl) GenerateRandomBinId(n int) string {
	for {
		id := RandomString(n)
		if !bowl.Exists(id) {
			return id
		}
	}
}

// GetPublicPastas returns a list of Public pasta IDs, stored in the bowl
func (bowl *PastaBowl) GetPublicPastas() ([]string, error) {
	ret := make([]string, 0)
	filename := fmt.Sprintf("%s/_public", bowl.Directory)
	if !FileExists(filename) {
		return ret, nil
	}

	file, err := os.OpenFile(filename, os.O_RDONLY, 0400)
	if err != nil {
		return ret, err
	}
	defer file.Close()
	// Read public pastas, one by one
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" {
			continue
		}
		ret = append(ret, line)
	}
	return ret, scanner.Err()
}

// WritePublicPastas writes a list of public pastas to the public file
func (bowl *PastaBowl) WritePublicPastaIDs(ids []string) error {
	filename := fmt.Sprintf("%s/_public", bowl.Directory)
	file, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0640)
	if err != nil {
		return err
	}
	defer file.Close()
	for _, id := range ids {
		file.Write([]byte(fmt.Sprintf("%s\n", id)))
	}
	return file.Sync()
}

func (bowl *PastaBowl) WritePublicPastas(pastas []Pasta) error {
	ids := make([]string, 0)
	for _, pasta := range pastas {
		ids = append(ids, pasta.Id)
	}
	return bowl.WritePublicPastaIDs(ids)
}
07070100000014000081A400000000000000000000000164BFCD7C000016A1000000000000000000000000000000000000002800000000pasta-0.7.2/cmd/pastad/storage__test.gopackage main

import (
	"io/ioutil"
	"math/rand"
	"os"
	"testing"
	"time"
)

var testBowl PastaBowl

func TestMain(m *testing.M) {
	// Initialisation
	rand.Seed(time.Now().UnixNano())
	testBowl.Directory = "pasta_test"
	os.Mkdir(testBowl.Directory, os.ModePerm)
	defer os.RemoveAll(testBowl.Directory)
	// Run tests
	ret := m.Run()
	os.Exit(ret)
}

func TestMetadata(t *testing.T) {
	var err error
	var pasta, p1, p2, p3 Pasta

	p1.Mime = "text/plain"
	p1.ExpireDate = 0
	if err = testBowl.InsertPasta(&p1); err != nil {
		t.Fatalf("Error inserting pasta 1: %s", err)
		return
	}
	if p1.Id == "" {
		t.Fatal("Pasta 1 id not set")
		return
	}
	if p1.Token == "" {
		t.Fatal("Pasta 1 id not set")
		return
	}
	p2.Mime = "application/json"
	p2.ExpireDate = time.Now().Unix() + 10000
	if err = testBowl.InsertPasta(&p2); err != nil {
		t.Fatalf("Error inserting pasta 2: %s", err)
		return
	}
	// Insert pasta with given ID and Token
	p3Id := testBowl.GenerateRandomBinId(12)
	p3Token := RandomString(20)
	p3.Id = p3Id
	p3.Token = p3Token
	p3.Mime = "text/rtf"
	p3.ExpireDate = time.Now().Unix() + 20000
	if err = testBowl.InsertPasta(&p3); err != nil {
		t.Fatalf("Error inserting pasta 3: %s", err)
		return
	}
	if p3.Id != p3Id {
		t.Fatal("Pasta 3 id mismatch")
		return
	}
	if p3.Token != p3Token {
		t.Fatal("Pasta 3 id mismatch")
		return
	}

	pasta, err = testBowl.GetPasta(p1.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 1: %s", err)
		return
	}
	if pasta != p1 {
		t.Fatal("Pasta 1 mismatch")
		return
	}
	pasta, err = testBowl.GetPasta(p2.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 2: %s", err)
		return
	}
	if pasta != p2 {
		t.Fatal("Pasta 2 mismatch")
		return
	}
	pasta, err = testBowl.GetPasta(p3.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 3: %s", err)
		return
	}
	if pasta != p3 {
		t.Fatal("Pasta 3 mismatch")
		return
	}

	if err = testBowl.DeletePasta(p1.Id); err != nil {
		t.Fatalf("Error deleting pasta 1: %s", err)
	}
	pasta, err = testBowl.GetPasta(p1.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 1 (after delete): %s", err)
		return
	}
	if pasta.Id != "" {
		t.Fatal("Pasta 1 exists after delete")
		return
	}
	// Ensure pasta 2 and 3 are not affected if we delete pasta 1
	pasta, err = testBowl.GetPasta(p2.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 2 after deleting pasta 1: %s", err)
		return
	}
	if pasta != p2 {
		t.Fatal("Pasta 2 mismatch after deleting pasta 1")
		return
	}
	pasta, err = testBowl.GetPasta(p3.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 3 after deleting pasta 1: %s", err)
		return
	}
	if pasta != p3 {
		t.Fatal("Pasta 3 mismatch after deleteing pasta 1")
		return
	}
	// Delete also pasta 2
	if err = testBowl.DeletePasta(p2.Id); err != nil {
		t.Fatalf("Error deleting pasta 2: %s", err)
	}
	pasta, err = testBowl.GetPasta(p2.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 2 (after delete): %s", err)
		return
	}
	if pasta.Id != "" {
		t.Fatal("Pasta 2 exists after delete")
		return
	}
	pasta, err = testBowl.GetPasta(p3.Id)
	if err != nil {
		t.Fatalf("Error getting pasta 3 after deleting pasta 2: %s", err)
		return
	}
	if pasta != p3 {
		t.Fatal("Pasta 3 mismatch after deleting pasta 2")
		return
	}
}

func TestBlobs(t *testing.T) {
	var err error
	var p1, p2 Pasta

	// Contents
	testString1 := RandomString(4096 * 8)
	testString2 := RandomString(4096 * 8)

	if err = testBowl.InsertPasta(&p1); err != nil {
		t.Fatalf("Error inserting pasta 1: %s", err)
		return
	}
	file, err := testBowl.GetPastaWriter(p1.Id)
	if err != nil {
		t.Fatalf("Error getting pasta file 1: %s", err)
		return
	}
	defer file.Close()
	if _, err = file.Write([]byte(testString1)); err != nil {
		t.Fatalf("Error writing to pasta file 1: %s", err)
		return
	}
	if err = file.Close(); err != nil {
		t.Fatalf("Error closing pasta file 1: %s", err)
		return
	}
	if err = testBowl.InsertPasta(&p2); err != nil {
		t.Fatalf("Error inserting pasta 2: %s", err)
		return
	}
	file, err = testBowl.GetPastaWriter(p2.Id)
	if err != nil {
		t.Fatalf("Error getting pasta file 2: %s", err)
		return
	}
	defer file.Close()
	if _, err = file.Write([]byte(testString2)); err != nil {
		t.Fatalf("Error writing to pasta file 2: %s", err)
		return
	}
	if err = file.Close(); err != nil {
		t.Fatalf("Error closing pasta file 2: %s", err)
		return
	}
	// Fetch contents now
	file, err = testBowl.GetPastaReader(p1.Id)
	if err != nil {
		t.Fatalf("Error getting pasta reader 1: %s", err)
		return
	}
	buf, err := ioutil.ReadAll(file)
	file.Close()
	if err != nil {
		t.Fatalf("Error reading pasta 1: %s", err)
		return
	}
	if testString1 != string(buf) {
		t.Fatal("Mismatch: pasta 1 contents")
		t.Logf("Bytes: Read %d, Expected %d", len(buf), len(([]byte(testString1))))
		return
	}
	// Same for pasta 2
	file, err = testBowl.GetPastaReader(p2.Id)
	if err != nil {
		t.Fatalf("Error getting pasta reader 2: %s", err)
		return
	}
	buf, err = ioutil.ReadAll(file)
	file.Close()
	if err != nil {
		t.Fatalf("Error reading pasta 2: %s", err)
		return
	}
	if testString2 != string(buf) {
		t.Fatal("Mismatch: pasta 2 contents")
		t.Logf("Bytes: Read %d, Expected %d", len(buf), len(([]byte(testString2))))
		return
	}

	// Check if pasta 1 can be deleted and the contents of pasta 2 are still OK afterwards
	if err = testBowl.DeletePasta(p1.Id); err != nil {
		t.Fatalf("Error deleting pasta 1: %s", err)
	}
	file, err = testBowl.GetPastaReader(p2.Id)
	if err != nil {
		t.Fatalf("Error getting pasta reader 2: %s", err)
		return
	}
	buf, err = ioutil.ReadAll(file)
	file.Close()
	if err != nil {
		t.Fatalf("Error reading pasta 2: %s", err)
		return
	}
	if testString2 != string(buf) {
		t.Fatal("Mismatch: pasta 2 contents")
		t.Logf("Bytes: Read %d, Expected %d", len(buf), len(([]byte(testString2))))
		return
	}

}
07070100000015000081A400000000000000000000000164BFCD7C000010FE000000000000000000000000000000000000002000000000pasta-0.7.2/cmd/pastad/utils.gopackage main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

// getenv reads a given environmental variable and returns it's value if present or defval if not present or empty
func getenv(key string, defval string) string {
	val := os.Getenv(key)
	if val == "" {
		return defval
	}
	return val
}

// getenv reads a given environmental variable as integer and returns it's value if present or defval if not present or empty
func getenv_i(key string, defval int) int {
	val := os.Getenv(key)
	if val == "" {
		return defval
	}
	if i32, err := strconv.Atoi(val); err != nil {
		return defval
	} else {
		return i32
	}
}

// getenv reads a given environmental variable as integer and returns it's value if present or defval if not present or empty
func getenv_i64(key string, defval int64) int64 {
	val := os.Getenv(key)
	if val == "" {
		return defval
	}
	if i64, err := strconv.ParseInt(val, 10, 64); err != nil {
		return defval
	} else {
		return i64
	}
}

func isAlphaNumeric(c rune) bool {
	return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9')
}

func containsOnlyAlphaNumeric(input string) bool {
	for _, c := range input {
		if !isAlphaNumeric(c) {
			return false
		}
	}
	return true
}

func removeNonAlphaNumeric(input string) string {
	ret := ""
	for _, c := range input {
		if isAlphaNumeric(c) {
			ret += string(c)
		}
	}
	return ret
}

func ExtractPastaId(path string) (string, error) {
	var id string
	i := strings.LastIndex(path, "/")
	if i < 0 {
		id = path
	} else {
		id = path[i+1:]
	}
	if !containsOnlyAlphaNumeric(id) {
		return "", fmt.Errorf("invalid id")
	}
	return id, nil
}

/* Load MIME types file. MIME types file is a simple text file that describes mime types based on file extenstions.
 * The format of the file is
 * EXTENSION = MIMETYPE
 */
func loadMimeTypes(filename string) (map[string]string, error) {
	ret := make(map[string]string, 0)

	file, err := os.OpenFile(filename, os.O_RDONLY, 0400)
	if err != nil {
		return ret, err
	}
	defer file.Close()
	scanner := bufio.NewScanner(file)
	for scanner.Scan() {
		line := strings.TrimSpace(scanner.Text())
		if line == "" || line[0] == '#' {
			continue
		}
		i := strings.Index(line, "=")
		if i < 0 {
			continue
		}
		name, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:])
		if name != "" && value != "" {
			ret[name] = value
		}
	}

	return ret, scanner.Err()
}

func takeFirst(arr []string) string {
	if len(arr) == 0 {
		return ""
	}
	return arr[0]
}

/* try to determine the mime type by file extension. Returns empty string on failure */
func mimeByFilename(filename string) string {
	i := strings.LastIndex(filename, ".")
	if i < 0 {
		return ""
	}
	extension := filename[i+1:]
	if mime, ok := mimeExtensions[extension]; ok {
		return mime
	}
	return ""
}

/* Extract the remote IP address of the given remote
 * The remote is expected to come from http.Request and contain the IP address plus the port */
func extractRemoteIP(remote string) string {
	// Check if IPv6
	i := strings.Index(remote, "[")
	if i >= 0 {
		j := strings.Index(remote, "]")
		if j <= i {
			return remote
		}
		return remote[i+1 : j]
	}
	i = strings.Index(remote, ":")
	if i > 0 {
		return remote[:i]
	}
	return remote
}
func timeHumanReadable(timestamp int64) string {
	if timestamp < 60 {
		return fmt.Sprintf("%d s", timestamp)
	}

	minutes := timestamp / 60
	seconds := timestamp - (minutes * 60)
	if minutes < 60 {
		return fmt.Sprintf("%d:%d min", minutes, seconds)
	}

	hours := minutes / 60
	minutes -= hours * 60
	if hours < 24 {
		return fmt.Sprintf("%d s", hours)
	}

	days := hours / 24
	hours -= days * 24
	if days > 365 {
		years := float32(days) / 365.0
		return fmt.Sprintf("%.2f years", years)
	} else if days > 28 {
		weeks := days / 7
		if weeks > 4 {
			months := days / 30
			return fmt.Sprintf("%d months", months)
		}
		return fmt.Sprintf("%d weeks", weeks)
	} else {
		return fmt.Sprintf("%d days", days)
	}
}

/* Apply custom macros in the given string and return the result. The following macros are supports:
* `$hostname` - Replace with the system hostname
 */
func ApplyMacros(txt string) (string, error) {
	if strings.Contains(txt, "$hostname") {
		hostname, err := os.Hostname()
		if err != nil {
			return "", err
		}
		txt = strings.ReplaceAll(txt, "$hostname", hostname)
	}
	return txt, nil
}
07070100000016000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001100000000pasta-0.7.2/docs07070100000017000081A400000000000000000000000164BFCD7C00000371000000000000000000000000000000000000001A00000000pasta-0.7.2/docs/build.md# Building

## Build and run from source

Use the provided `Makefile` commands:

    make                                           # all

    make pastad                                    # Server
    make pasta                                     # Client

	make static                                    # build static binaries (for release)

## Build docker image

    make docker

Or manually:

    docker build . -t feldspaten.org/pasta         # Build docker container

Create or run the container with

    docker container create --name pasta -p 8199:8199 -v ABSOLUTE_PATH_TO_DATA_DIR:/data feldspaten.org/pasta
    docker container run --name pasta -p 8199:8199 -v ABSOLUTE_PATH_TO_DATA_DIR:/data feldspaten.org/pasta

The container needs a `data` directory with a valid `pastad.toml` (See the [example file](pastad.toml.example), otherwise default values will be used).07070100000018000081A400000000000000000000000164BFCD7C0000050D000000000000000000000000000000000000002900000000pasta-0.7.2/docs/cloud-init.yaml.examplessh_authorized_keys:
  - ssh-rsa ...
mounts:
  - ["/dev/sdb1", "/data", "ext4", ""]
write_files:
  - path: /mnt/pastad.toml
    permissions: "0755"
    owner: root
    content: |
      BaseURL = "https://pasta.domain.com" # replace with your hostname
      PastaDir = "pastas"                  # absolute or relative path to the pastas
      BindAddress = ":8199"                # server bind address
      MaxPastaSize = 26214400              # max allowed pasta size - 5 MB
      PastaCharacters = 8                  # Number of characters for pasta id
      Expire = 2592000                     # Default expire in seconds (1 Month)
      Cleanup = 3600                       # Cleanup interval in seconds (1 hour)
runcmd:
  - sudo wget https://raw.githubusercontent.com/grisu48/pasta/main/mime.types -O /data/mime.types
- sudo cp /mnt/pastad.toml /data/pastad.toml
rancher:
  network:
    dns:
      nameservers:
        - 8.8.8.8
        - 1.1.1.1
    interfaces:
      eth0:
        addresses:
          - 192.0.2.2/24
          - 2001:db8::2/64
        gateway: 192.0.2.1
        gateway_ipv6: 2001:db8::1
        mtu: 1500
        dhcp: false
  services:
    past:
      image: grisu48/pasta
      volumes:
        - /data:/data
      ports:
        - "80:8199"
      restart: always

07070100000019000081A400000000000000000000000164BFCD7C00000486000000000000000000000000000000000000002400000000pasta-0.7.2/docs/getting-started.md# Installation

The easiest way is to run `pasta` as a container service or get the pre-build binaries from the releases within this repository.

If you prefer the native applications, checkout the sections below.

## Install on openSUSE

openSUSE packages are provided at [build.opensuse.org](https://build.opensuse.org/package/show/home%3Aph03nix%3Atools/pasta).
To install follow the instructions from [software.opensuse.org](https://software.opensuse.org/download/package?package=pasta&project=home%3Aph03nix%3Atools) or the following snippet:

	# Tumbleweed
    zypper addrepo zypper addrepo https://download.opensuse.org/repositories/home:ph03nix:tools/openSUSE_Tumbleweed/home:ph03nix:tools.repo
    zypper refresh && zypper install pasta

## RancherOS

Let's assume we have `/dev/sda` for the system and `/dev/sdb` for data.

* Prepare persistent storage for data
* Install the system with given [`cloud-init.yaml`](cloud-init.yaml.example) to system storage
* Configure your proxy and enojoy!

```bash
$ sudo parted /dev/sdb
  # mktable - gpt - mkpart - 1 - 0% - 100%
$ sudo mkfs.ext4 /dev/sdb1
$ sudo ros install -d /dev/sda -c cloud-init.yaml
```
0707010000001A000081A400000000000000000000000164BFCD7C000000C8000000000000000000000000000000000000001A00000000pasta-0.7.2/docs/index.md# pasta documentation

This folder contains auxilliary documentation for pasta.
Checkout one of the following sections.

* [Getting started guides](getting-started.md)
* [Build instructions](build.md)0707010000001B000081A400000000000000000000000164BFCD7C0000007F000000000000000000000000000000000000001300000000pasta-0.7.2/go.modmodule github.com/grisu48/pasta

go 1.13

require (
	github.com/BurntSushi/toml v1.3.2
	github.com/akamensky/argparse v1.4.0
)
0707010000001C000081A400000000000000000000000164BFCD7C0000015C000000000000000000000000000000000000001300000000pasta-0.7.2/go.sumgithub.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
0707010000001D000081A400000000000000000000000164BFCD7C000005CA000000000000000000000000000000000000001700000000pasta-0.7.2/mime.types# Known mime types based on file extension
bmp = image/bmp
bz = application/x-bzip
bz2 = application/x-bzip2
csh = application/x-csh
css = text/csvv
csv = text/csv
doc = application/msword
docx = application/vnd.openxmlformats-officedocument.wordprocessingml.document
epub = application/epub+zip
gz = application/gzip
gif = image/gif
htm = text/html
html = text/html
ics = text/calendar
jar = application/java-archive
jpg = image/jpeg
jpeg = image/jpeg
js = text/javascript
json = application/json
mp3 = audio/mpeg
mpg = audio/mpeg
mpeg = audio/mpeg
ods = application/vnd.oasis.opendocument.presentation
otd = application/vnd.oasis.opendocument.spreadsheet
otd = application/vnd.oasis.opendocument.text
oga = audio/ogg
ogv = audio/ogg
opus = audio/opus
otf = font/otf
png = image/png
pdf = application/pdf
php = application/x-httpd-php
ppt = application/vnd.ms-powerpoint
pptx = application/vnd.openxmlformats-officedocument.presentationml.presentation
rat = application/vnd.rar
rtf = application/rtf
sh = application/x-sh
svg = image/svg+xml
tar = application/x-tar
tif = image/tiff
tiff = image/tiff
ts = video/mp2t
ttf = font/ttf
txt = text/plain
wav = audio/wav
weba = audio/webm
webm = video/webm
webp = image/webp
woff = font/woff
woff2 = font/woff2
xhtml = application/xhtml+xml
xls = application/vnd.ms-excel
xlsx = application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xml = text/xml
xz = application/x-xz
zip = application/zip
7z = application/x-7z-compressed
0707010000001E000081A400000000000000000000000164BFCD7C000001FC000000000000000000000000000000000000001F00000000pasta-0.7.2/pasta.toml.example# Keep in mind to escape string. This is TOML not an ini file!
# Place this file in ~/.pasta.toml

RemoteHost = "http://localhost:8199"

# Example for a remote with one alias
# aliases can be given to `pasta` as a remote argument and will be
#   completed to the given url
[[Remote]]
url = "http://localhost:8199"
alias = "localhost"

# Example of a remote with multiple aliases
[[Remote]]
url = ""http://localhost:8200"
alias = "localhost2"          # one alias
aliases = ["local2", "loc2"]  # more aliases
0707010000001F000081A400000000000000000000000164BFCD7C000002CA000000000000000000000000000000000000002000000000pasta-0.7.2/pastad.toml.exampleBaseURL = "http://localhost:8199"    # base URL as used within pasta
BindAddress = ":8199"                # bind address
PastaDir = "pastas"                  # absolute or relative path to the pastas data directory
MaxPastaSize = 5242880               # max allowed pasta size (5 MiB)
PastaCharacters = 8                  # Number of characters for pasta id
Expire = 2592000                     # Default expire in seconds (1 Month)
Cleanup = 3600                       # Cleanup interval in seconds (1 hour)
RequestDelay = 2000                  # Milliseconds between POST/DELETE requests per host
PublicPastas = 0                     # Number of public pastas to display or 0 to disable public display (default)
07070100000020000041ED00000000000000000000000264BFCD7C00000000000000000000000000000000000000000000001100000000pasta-0.7.2/test07070100000021000081ED00000000000000000000000164BFCD7C00000EC1000000000000000000000000000000000000001900000000pasta-0.7.2/test/test.sh#!/bin/bash -e
# Summary: Function test for pasta & pastad

PASTAS=~/.pastas.dat               # pasta client dat file
PASTAS_TEMP=""                     # temp file, if present

function cleanup() {
	set +e
	# restore old pasta client file
	if [[ $PASTAS_TEMP != "" ]]; then
		mv "$PASTAS_TEMP" "$PASTAS"
	fi
	rm -f testfile
	rm -f testfile2
	rm -f rm
	kill %1
	rm -rf pasta_test
	rm -f pasta.json
	rm -f test_config.toml
}

trap cleanup EXIT

## Preparation: Safe old pastas.dat, if existing
if [[ -s $PASTAS ]]; then
	PASTAS_TEMP=`mktemp`
	mv "$PASTAS" "$PASTAS_TEMP"
fi

## Setup pasta server
../pastad -c pastad.toml -m ../mime.types -B http://127.0.0.1:8200 -b 127.0.0.1:8200 &
sleep 2

## Push a testfile
echo "Testfile 123" > testfile
link=`../pasta -r http://127.0.0.1:8200 < testfile`
curl --fail -o testfile2 $link
diff testfile testfile2
echo "Testfile matches"
echo "Testfile 123456789" > testfile
link=`../pasta -r http://127.0.0.1:8200 < testfile`
curl --fail -o testfile2 $link
diff testfile testfile2
echo "Testfile 2 matches"
# Test also sending via curl
url=`curl --fail -X POST http://127.0.0.1:8200/ --data-binary @testfile | grep -Eo 'http://.*'`
echo "curl stored as $url"
curl --fail -o testfile3 "$url"
diff testfile testfile3
echo "Testfile 3 matches"
# Test the POST form
echo -n "testpasta" > testfile4
url=`curl --fail -X POST "http://127.0.0.1:8200?input=form&content=testpasta" | grep -Eo 'http://.*'`
curl --fail -o testfile5 "$url"
diff testfile4 testfile5
# Test different format in link
curl --fail -X POST http://127.0.0.1:8200?ret=json --data-binary @testfile

## Second pasta server with environment variables
echo "Testing environment variables ... "
PASTA_BASEURL=pastas PASTA_BINDADDR=127.0.0.1:8201 PASTA_CHARACTERS=12 ../pastad -m ../mime.types &
SECONDPID=$!
sleep 2        # TODO: Don't do sleep here you lazy ... :-)
link2=`../pasta -r http://127.0.0.1:8201 < testfile`
curl --fail -o testfile_second $link
diff testfile testfile_second
kill $SECONDPID

## Test spam protection
echo "Testing spam protection ... "
../pasta -r http://127.0.0.1:8200 testfile >/dev/null
! timeout 1 ../pasta -r http://127.0.0.1:8200 testfile >/dev/null
sleep 2
../pasta -r http://127.0.0.1:8200 testfile >/dev/null

## TODO: Test expire pasta cleanup

## Test special commands
function test_special_command() {
	command="$1"
	echo "test" > $command
	# Ambiguous, if the shortcut command and a similar file exists. This must fail
	! ../pasta -r http://127.0.0.1:8200 "$command"
	# However it must pass, if the file is explicitly stated
	../pasta -r http://127.0.0.1:8200 -f "$command"
	# And it must succeed, if there is no such file and thus is it clear what should happen
	if [[ "$command" != "rm" ]]; then rm "$command"; fi
	../pasta -r http://127.0.0.1:8200 "$command"
}
test_special_command "ls"
test_special_command "rm"
test_special_command "gc"

## Test creation of default config
rm -f test_config.toml
../pastad -c test_config.toml -B http://127.0.0.1:8201 -b 127.0.0.1:8201 &
sleep 2 # TODO: Don't sleep here either but create a valid monitor
kill %2
stat test_config.toml
# Ensure the test config contains the expected entries
grep 'BaseURL[[:space:]]=' test_config.toml
grep 'BindAddress[[:space:]]*=' test_config.toml
grep 'PastaDir[[:space:]]*=' test_config.toml
grep 'MaxPastaSize[[:space:]]*=' test_config.toml
grep 'PastaCharacters[[:space:]]*=' test_config.toml
grep 'Expire[[:space:]]*=' test_config.toml
grep 'Cleanup[[:space:]]*=' test_config.toml
grep 'RequestDelay[[:space:]]*=' test_config.toml
echo "test_config.toml has been successfully created"

## Check the date handling of the pasta client
## Ensure there are no 1970 entries in ls
! ../pasta ls | grep '1970'
../pasta ls | grep `date +"%Y-%m-%d"`

echo "All good :-)"
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!168 blocks
openSUSE Build Service is sponsored by