forked from electricdusk/rushlink
Compare commits
58 Commits
dsprenkels
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| dbb6a954e1 | |||
|
|
2c889e0808 | ||
|
|
737a26fee3 | ||
|
|
016ffa8949 | ||
|
|
b9119a0df5 | ||
|
|
a4cec1e4b0 | ||
|
|
b7ea5dfa4f | ||
|
|
a0c8383555 | ||
| bbe787da5d | |||
| 28ddaee9d9 | |||
|
|
01adfa8f2f | ||
|
|
c57e719e15 | ||
|
|
c0e4ac2c40 | ||
|
|
b73317c249 | ||
|
|
8e89955ce9 | ||
|
|
e476797da0 | ||
|
|
728d3833c3 | ||
|
|
42ccc18002 | ||
|
|
63a588ba59 | ||
|
|
3da165a57b | ||
|
|
09481f47e6 | ||
|
|
1dd0d17ba5 | ||
|
|
7c0bfaee76 | ||
|
|
a766d5d596 | ||
|
|
c28dfd0cb4 | ||
| 732b1fc2a6 | |||
| 8403ad2258 | |||
|
|
ad1ce67495 | ||
| 11ea63e1fc | |||
|
|
0a35cb2508 | ||
|
|
de19234108 | ||
| 9a690e2b8b | |||
| ac2c62f9e6 | |||
|
|
095348d614 | ||
|
|
245dd64f82 | ||
|
|
5e6ce9c2be | ||
|
|
8dce4e8483 | ||
|
|
ffeb9a3362 | ||
|
|
3d07acb222 | ||
|
|
c46a26f8a2 | ||
|
|
087b9920e6 | ||
|
|
ca859adab1 | ||
|
|
d34ac11d5e | ||
|
|
824c6f41e2 | ||
|
|
f32e47b4c8 | ||
|
|
8cbe984ba4 | ||
|
|
0bffde1dc1 | ||
|
|
41f4de43ac | ||
|
|
728b5d9d4b | ||
|
|
40a32fa535 | ||
|
|
ba08aca622 | ||
|
|
76cf92e22d | ||
|
|
62e82d831e | ||
|
|
eec5e4def4 | ||
|
|
f9c74a83f0 | ||
|
|
da5806a6f7 | ||
|
|
f1fe160655 | ||
|
|
cf956501ac |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -14,8 +14,12 @@
|
|||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
# Generated code from assets using bindata
|
|
||||||
bindata.go
|
|
||||||
|
|
||||||
# Output binary
|
# Output binary
|
||||||
/rushlink
|
/rushlink
|
||||||
|
|
||||||
|
# Any kind of backup files
|
||||||
|
*~
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# Visual Code local config directory
|
||||||
|
.vscode
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -2,12 +2,53 @@
|
|||||||
|
|
||||||
A URL shortener and (maybe) a pastebin server for our #ru community.
|
A URL shortener and (maybe) a pastebin server for our #ru community.
|
||||||
|
|
||||||
## Build instructions
|
## Building
|
||||||
|
|
||||||
- `go get -u github.com/go-bindata/go-bindata/...`
|
- `go get -u github.com/go-bindata/go-bindata/...`
|
||||||
- `go generate ./...`
|
- `go generate ./...`
|
||||||
- `go build ./cmd/rushlink`
|
- `go build ./cmd/rushlink`
|
||||||
|
|
||||||
|
## Deploying
|
||||||
|
|
||||||
|
We recommend running `rushlink` behind a reverse proxy suitable for processing
|
||||||
|
HTTP requests, such as `nginx`, or `haproxy`.
|
||||||
|
|
||||||
|
## Sample `nginx` config
|
||||||
|
|
||||||
|
```
|
||||||
|
server {
|
||||||
|
location / {
|
||||||
|
root /var/www/rushlink;
|
||||||
|
proxy_pass http://127.0.0.1:8000;
|
||||||
|
proxy_set_header Host rushlink.local;
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`rushlink` automatically detects whether `http` or `https` is used when
|
||||||
|
`X-Forwarded-Proto` is correctly set. Otherwise, pass `-root_url
|
||||||
|
https://rushlink.local` to the binary (e.g. in the `systemd` unit file).
|
||||||
|
|
||||||
|
## Sample `systemd` unit file
|
||||||
|
|
||||||
|
```
|
||||||
|
[Install]
|
||||||
|
WantedBy=nginx.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=rushlink
|
||||||
|
Group=nogroup
|
||||||
|
ExecStart=/var/lib/rushlink/rushlink -database /var/lib/rushlink/db -file-store /var/lib/rushlink/filestore -root_url https://rushlink.local
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Background
|
||||||
|
|
||||||
## Libraries
|
## Libraries
|
||||||
|
|
||||||
Use standard-Go-libraries if the job can be done with those. As of now, these
|
Use standard-Go-libraries if the job can be done with those. As of now, these
|
||||||
@@ -24,7 +65,7 @@ are the exceptions:
|
|||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
We will be using [`go.etcd.io/bbolt`]. This file should be the *only* file
|
We use [`go.etcd.io/bbolt`]. This file should be the *only* file
|
||||||
apart from our monolithic binary. All settings and keys should go in here.
|
apart from our monolithic binary. All settings and keys should go in here.
|
||||||
Any read-only data resides in the binary file (possibly compressed).
|
Any read-only data resides in the binary file (possibly compressed).
|
||||||
|
|
||||||
@@ -105,17 +146,3 @@ header that the client sends. We can still wrap the plain-text page in a single
|
|||||||
We will try as hard as possible to not store any data about our users, and will
|
We will try as hard as possible to not store any data about our users, and will
|
||||||
only provide any data when we have the legal obligation to do so.
|
only provide any data when we have the legal obligation to do so.
|
||||||
|
|
||||||
## Sample `nginx` config
|
|
||||||
|
|
||||||
```
|
|
||||||
server {
|
|
||||||
location / {
|
|
||||||
root /var/www/rushlink;
|
|
||||||
proxy_pass http://127.0.0.1:8000;
|
|
||||||
proxy_set_header Host rushlink.local;
|
|
||||||
proxy_set_header X-Forwarded-For $remote_addr;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|||||||
34
assets/css/main.css
Normal file
34
assets/css/main.css
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
body {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dropZone {
|
||||||
|
background-color: rgba(0, 0, 0.2, 0.75);
|
||||||
|
color: white;
|
||||||
|
font-size: 60px;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 100%;
|
||||||
|
left: 0;
|
||||||
|
padding: 1em;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
transition: visibility 175ms, opacity 175ms;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding-left: 4ex; /* approx 4 monospaced spaces */
|
||||||
|
}
|
||||||
|
|
||||||
|
.success {
|
||||||
|
color: #008000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fail {
|
||||||
|
color: #800000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
51
assets/js/copyclipboard.js
Normal file
51
assets/js/copyclipboard.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const COPY_TO_CLIPBOARD_CONTAINER_ID = "copyToClipboardContainer";
|
||||||
|
const COPY_TO_CLIPBOARD_STATUS_ID = "copyToClipboardStatus";
|
||||||
|
let copyToClipboardCtr = 0;
|
||||||
|
|
||||||
|
function copyToClipboardSuccess() {
|
||||||
|
let statusElem = document.getElementById(COPY_TO_CLIPBOARD_STATUS_ID);
|
||||||
|
statusElem.innerText = "URL copied!";
|
||||||
|
statusElem.classList.remove("fail");
|
||||||
|
statusElem.classList.add("success");
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboardFail(cause) {
|
||||||
|
let statusElem = document.getElementById(COPY_TO_CLIPBOARD_STATUS_ID);
|
||||||
|
if (!cause) cause = "unknown error";
|
||||||
|
let msg = "copy failed: " + cause;
|
||||||
|
console.log(msg);
|
||||||
|
statusElem.innerText = msg;
|
||||||
|
statusElem.classList.remove("success");
|
||||||
|
statusElem.classList.add("fail");
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyToClipboard(url) {
|
||||||
|
let copyEventIdx = ++copyToClipboardCtr;
|
||||||
|
|
||||||
|
if (!window.navigator.clipboard) {
|
||||||
|
copyToClipboardFail("could not access clipboard");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.navigator.clipboard.writeText(url).then(() => {
|
||||||
|
if (copyToClipboardCtr !== copyEventIdx) return;
|
||||||
|
copyToClipboardSuccess();
|
||||||
|
}, () => {
|
||||||
|
if (copyToClipboardCtr !== copyEventIdx) return;
|
||||||
|
copyToClipboardFail();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
if (!window.navigator.clipboard) {
|
||||||
|
let msg = "cannot access clipboard";
|
||||||
|
if (window.location.protocol !== "https:") {
|
||||||
|
msg += ": website not using https"
|
||||||
|
}
|
||||||
|
console.error(msg);
|
||||||
|
} else {
|
||||||
|
let container = document.getElementById(COPY_TO_CLIPBOARD_CONTAINER_ID);
|
||||||
|
container.classList.remove("hidden");
|
||||||
|
}
|
||||||
|
})();
|
||||||
48
assets/js/dragdrop.js
vendored
Normal file
48
assets/js/dragdrop.js
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', (event) => {
|
||||||
|
// The implementation of this drag-drop logic is largely based on the
|
||||||
|
// answer described in <https://stackoverflow.com/a/28226022/5207081>.
|
||||||
|
//
|
||||||
|
// The gist is this: every time the drag enters a new DOM object, it will
|
||||||
|
// fire a new 'dragenter' event on that element. That is a pain, because
|
||||||
|
// we do not know which element will be the last to receive a dragleave
|
||||||
|
// event, in case the user aborts the process.
|
||||||
|
//
|
||||||
|
// Therefore, we use an Ugly Hack™. There will be an element with id
|
||||||
|
// 'dropZone', which blocks the complete page. When a 'dragenter' event
|
||||||
|
// is received on any DOM element, we will unhide this modal page-blocker.
|
||||||
|
// This will guarantee us that the next 'enter' event will be received on
|
||||||
|
// this page-block element. This event we shall ignore.
|
||||||
|
//
|
||||||
|
// Then, whenever the user stop their drag action, we will receive the
|
||||||
|
// 'dragleave' on the '#dropZone' element. Now we know when exactly
|
||||||
|
// a drag action is cancelled (and we will hide the modal element
|
||||||
|
// accordingly).
|
||||||
|
|
||||||
|
let dropZone = document.getElementById("dropZone");
|
||||||
|
window.addEventListener("dragenter", (event) => {
|
||||||
|
// User starts a drag action
|
||||||
|
dropZone.style.visibility = "";
|
||||||
|
dropZone.style.opacity = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener("drop", (event) => {
|
||||||
|
// User drops a file
|
||||||
|
event.preventDefault();
|
||||||
|
document.getElementById("fileUploadField").files = event.dataTransfer.files;
|
||||||
|
document.getElementById("fileUploadForm").submit();
|
||||||
|
});
|
||||||
|
|
||||||
|
dropZone.addEventListener("dragleave", (event) => {
|
||||||
|
// User cancels drag action
|
||||||
|
dropZone.style.visibility = "hidden";
|
||||||
|
dropZone.style.opacity = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("dragover", (event) => {
|
||||||
|
// Prevent the browser from opening the file, because of a drop event
|
||||||
|
// on the window. (<https://stackoverflow.com/a/6756680/5207081>)
|
||||||
|
event.preventDefault();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>{{block "title" .}}rushlink{{end}}</title>{{block "head-append" .}}{{end}}
|
<title>{{block "title" .}}rushlink{{end}}</title>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/main.css" />
|
||||||
|
{{block "head-append" .}}{{end -}}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
{{block "body" .}}
|
{{block "body" .}}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{{define "title"}}
|
|
||||||
Success - rushlink
|
|
||||||
{{end}}
|
|
||||||
@@ -1,20 +1,47 @@
|
|||||||
{{define "body"}}
|
{{define "head-append"}}
|
||||||
<pre>
|
<script type="text/javascript" src="/js/dragdrop.js" defer></script>
|
||||||
#RU URL SHORTENER
|
{{end}}
|
||||||
=================
|
|
||||||
|
|
||||||
Based on https://0x0.st/, this site allows you to easily shorten URLs using
|
{{define "body"}}
|
||||||
|
<div style="visibility:hidden; opacity:0" id="dropZone">File incoming! :D</div>
|
||||||
|
<h1>#RU paste-dump</h1>
|
||||||
|
|
||||||
|
Based on https://0x0.st/, this site allows you to easily upload files and shorten URLs using
|
||||||
the command line.
|
the command line.
|
||||||
|
|
||||||
## USAGE
|
<h2>Web-API</h2>
|
||||||
|
<section>
|
||||||
|
<form id="fileUploadForm" action="/" method="post" enctype="multipart/form-data">
|
||||||
|
<label class="formLabel" for="fileUploadField">Upload a file:</label>
|
||||||
|
<input id="fileUploadField" class="formMain" name="file" type="file" />
|
||||||
|
<input id="fileUploadField" class="formSubmit" type="submit" value="upload!" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<form id="shortenURLForm" action="/" method="post" enctype="multipart/form-data">
|
||||||
|
<label class="formLabel" for="shortenURLField">Upload a URL:</label>
|
||||||
|
<input id="shortenURLField" class="formMain" name="shorten" type="url" />
|
||||||
|
<input id="shortenURLSubmit" class="formSubmit" type="submit" value="shorten!" />
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
# Upload a file
|
<h2>Command line API</h2>
|
||||||
curl -F'file=@yourfile.png' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a>
|
<pre>
|
||||||
|
# Upload a file
|
||||||
|
curl -F'file=@yourfile.png' <a href="{{.RootURL}}">{{.RootURL}}</a>
|
||||||
|
|
||||||
# Shorten a URL
|
# Shorten a URL
|
||||||
curl -F'shorten=http://example.com/some/long/url' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a>
|
curl -F'shorten=http://example.com/some/long/url' <a href="{{.RootURL}}">{{.RootURL}}</a>
|
||||||
|
|
||||||
# Shorten a URL with a token to delete it later
|
# The first line of the result will contain the shortened URL.
|
||||||
curl -F'shorten=http://example.com/some/long/url' -F'deleteToken=' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a>
|
#
|
||||||
|
# In the other lines, you will find other information, including
|
||||||
|
# information on how to delete the shortened object.
|
||||||
|
|
||||||
|
# Upload output from another process
|
||||||
|
figlet Rushlink | curl -F'file=@-' <a href="{{.RootURL}}">{{.RootURL}}</a>
|
||||||
|
|
||||||
|
# To upload a file and only extract the shortened URL (i.e. throw away the rest)
|
||||||
|
curl -F'file=@yourfile.png' <a href="{{.RootURL}}">{{.RootURL}}</a> | head -n 1
|
||||||
</pre>
|
</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{{define "title"}}
|
|
||||||
Success - rushlink
|
|
||||||
{{end}}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{{define "title"}}
|
|
||||||
Success - rushlink
|
|
||||||
{{end}}
|
|
||||||
@@ -1,3 +1,37 @@
|
|||||||
{{define "title"}}
|
{{define "title"}}
|
||||||
'{{.Paste.Key}}' meta info - rushlink
|
'{{.Paste.Key}}{{.FileExt}}' metadata - rushlink
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "head-append"}}
|
||||||
|
<script type="text/javascript" src="/js/copyclipboard.js" defer></script>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{define "body"}}
|
||||||
|
<pre id="copyToClipboardContainer" class="hidden">
|
||||||
|
<button onclick="copyToClipboard('{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}')">Copy URL to clipboard</button> <span id="copyToClipboardStatus"></span>
|
||||||
|
</pre>
|
||||||
|
<pre>
|
||||||
|
<a href="{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}">{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}</a>
|
||||||
|
---
|
||||||
|
{{if and (ne .Paste.State.String "deleted") .CanDeleteBool}}
|
||||||
|
with delete token: <a href="{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}">{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}</a>
|
||||||
|
{{else -}}
|
||||||
|
with delete token: <unknown>
|
||||||
|
{{end -}}
|
||||||
|
type: {{.Paste.Type}}
|
||||||
|
state: {{.Paste.State}}
|
||||||
|
{{if .Paste.TimeCreated.IsZero -}}
|
||||||
|
created: unknown
|
||||||
|
{{else -}}
|
||||||
|
created: {{.Paste.TimeCreated}}
|
||||||
|
{{end -}}
|
||||||
|
delete token: {{.CanDeleteString}}
|
||||||
|
<form action="delete?deleteToken={{.Paste.DeleteToken}}" method="post">
|
||||||
|
<input type="submit" name="delete" value="Delete paste"
|
||||||
|
{{- if or (eq .Paste.State.String "deleted") (not .CanDeleteBool) -}}
|
||||||
|
disabled
|
||||||
|
{{- end -}}
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</pre>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -1 +0,0 @@
|
|||||||
<{{.Request.Host}}/{{.Paste.Key}}> was succesfully deleted
|
|
||||||
@@ -6,11 +6,19 @@ the command line.
|
|||||||
|
|
||||||
## USAGE
|
## USAGE
|
||||||
|
|
||||||
# Upload a file
|
# Upload a file
|
||||||
curl -F'file=@yourfile.png' https://{{.Request.Host}}
|
curl -F'file=@yourfile.png' {{.RootURL}}
|
||||||
|
|
||||||
# Shorten a URL
|
# Shorten a URL
|
||||||
curl -F'shorten=http://example.com/some/long/url' https://{{.Request.Host}}
|
curl -F'shorten=http://example.com/some/long/url' {{.RootURL}}
|
||||||
|
|
||||||
# Shorten a URL with a token to delete it later
|
# The first line of the result will contain the shortened URL.
|
||||||
curl -F'shorten=http://example.com/some/long/url' -F'deleteToken=' https://{{.Request.Host}}
|
#
|
||||||
|
# In the other lines, you will find other information, including
|
||||||
|
# information on how to delete the shortened object.
|
||||||
|
|
||||||
|
# Upload output from another process
|
||||||
|
figlet Rushlink | curl -F'file=@-' {{.RootURL}}
|
||||||
|
|
||||||
|
# To upload a file and only extract the shortened URL (i.e. throw away the rest)
|
||||||
|
curl -F'file=@yourfile.png' {{.RootURL}} | head -n 1
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
{{if .Request.PostForm.deleteToken -}}
|
|
||||||
https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}
|
|
||||||
{{else -}}
|
|
||||||
https://{{.Request.Host}}/{{.Paste.Key}}
|
|
||||||
{{end -}}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{{if .Request.PostForm.deleteToken -}}
|
|
||||||
https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}}
|
|
||||||
{{else -}}
|
|
||||||
https://{{.Request.Host}}/{{.Paste.Key}}
|
|
||||||
{{end -}}
|
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
METADATA on <{{.Request.Host}}/{{.Paste.Key}}>:
|
{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}
|
||||||
|
---
|
||||||
TYPE: {{.Paste.Type}}
|
{{if and (ne .Paste.State.String "deleted") .CanDeleteBool}}
|
||||||
STATE: {{.Paste.State}}
|
with delete token: {{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}
|
||||||
{{if .Paste.TimeCreated.IsZero -}}
|
|
||||||
CREATED: undefined
|
|
||||||
{{else -}}
|
{{else -}}
|
||||||
CREATED: {{.Paste.TimeCreated}}
|
with delete token: <unknown>
|
||||||
|
{{end -}}type: {{.Paste.Type}}
|
||||||
|
state: {{.Paste.State}}
|
||||||
|
{{if .Paste.TimeCreated.IsZero -}}
|
||||||
|
created: unknown
|
||||||
|
{{else -}}
|
||||||
|
created: {{.Paste.TimeCreated}}
|
||||||
{{end -}}
|
{{end -}}
|
||||||
DELETE TOKEN: {{.CanDelete.String}}
|
delete token: {{.CanDeleteString}}
|
||||||
|
|
||||||
{{if and (ne .Paste.State.String "deleted") .CanDelete.Bool}}
|
{{if and (ne .Paste.State.String "deleted") .CanDeleteBool}}
|
||||||
```
|
```
|
||||||
# To delete this {{.Paste.Type}}, execute:
|
# To delete this {{.Paste.Type}}, execute:
|
||||||
curl --request "DELETE" "{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Request.URL.Query.Get "deleteToken"}}"
|
curl --request "DELETE" "{{.RootURL}}/{{.Paste.Key}}{{.FileExt}}?deleteToken={{.Paste.DeleteToken}}"
|
||||||
```
|
```
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|||||||
484
bindata.go
Normal file
484
bindata.go
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
// Code generated for package rushlink by go-bindata DO NOT EDIT. (@generated)
|
||||||
|
// sources:
|
||||||
|
// assets/css/main.css
|
||||||
|
// assets/js/copyclipboard.js
|
||||||
|
// assets/js/dragdrop.js
|
||||||
|
// assets/templates/html/base.html.tmpl
|
||||||
|
// assets/templates/html/error.html.tmpl
|
||||||
|
// assets/templates/html/index.html.tmpl
|
||||||
|
// assets/templates/html/pasteMeta.html.tmpl
|
||||||
|
// assets/templates/txt/base.txt.tmpl
|
||||||
|
// assets/templates/txt/error.txt.tmpl
|
||||||
|
// assets/templates/txt/index.txt.tmpl
|
||||||
|
// assets/templates/txt/pasteMeta.txt.tmpl
|
||||||
|
package rushlink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func bindataRead(data []byte, name string) ([]byte, error) {
|
||||||
|
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
_, err = io.Copy(&buf, gz)
|
||||||
|
clErr := gz.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Read %q: %v", name, err)
|
||||||
|
}
|
||||||
|
if clErr != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type asset struct {
|
||||||
|
bytes []byte
|
||||||
|
info os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type bindataFileInfo struct {
|
||||||
|
name string
|
||||||
|
size int64
|
||||||
|
mode os.FileMode
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name return file name
|
||||||
|
func (fi bindataFileInfo) Name() string {
|
||||||
|
return fi.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size return file size
|
||||||
|
func (fi bindataFileInfo) Size() int64 {
|
||||||
|
return fi.size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode return file mode
|
||||||
|
func (fi bindataFileInfo) Mode() os.FileMode {
|
||||||
|
return fi.mode
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mode return file modify time
|
||||||
|
func (fi bindataFileInfo) ModTime() time.Time {
|
||||||
|
return fi.modTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir return file whether a directory
|
||||||
|
func (fi bindataFileInfo) IsDir() bool {
|
||||||
|
return fi.mode&os.ModeDir != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sys return file is sys mode
|
||||||
|
func (fi bindataFileInfo) Sys() interface{} {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _cssMainCss = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x51\xd1\x6e\xc3\x20\x0c\x7c\xcf\x57\x58\xaa\x26\x6d\x55\xd3\xd2\xa9\x5d\x57\xf2\x27\x7b\x23\xc1\x49\xac\x11\x8c\x80\xae\x49\xa7\xfe\xfb\x94\x84\xac\xd3\x10\x02\x7c\x77\x36\x27\xbb\x64\x3d\xc0\x77\x06\x00\x50\xb3\x8d\x79\xad\x3a\x32\x83\x84\x8e\x2d\x07\xa7\x2a\x2c\xb2\x7b\x96\xad\xb4\x67\xf7\xc1\x16\x93\xb4\x54\xd5\x67\xe3\xf9\x62\x75\x5e\xb1\x61\x2f\xc1\x37\xa5\x7a\x16\x1b\x18\xf7\xf6\x75\x3c\x4e\xc7\x97\x62\x12\x27\xc5\xb5\xa5\x88\xc5\xe3\xa7\x40\x37\x94\xf0\x26\x5c\xff\x07\xbc\x22\x35\x6d\x94\x50\xb2\xd1\x33\xdc\x26\x64\x2f\xc4\x53\x01\xcb\x9a\x28\x83\x75\x94\x20\x8a\x39\x72\x4a\x6b\xb2\x8d\x84\x3d\x76\x73\xaa\xe3\x40\x91\xd8\x4a\xa8\xa9\xc7\x54\x2f\xb2\x1b\x73\xe6\xb7\x57\x76\x91\x7c\x51\xa0\x92\x0c\xc5\x01\xf6\xa7\x63\x17\x36\xc0\x4e\x55\xbf\xe1\x9c\x70\x25\x1d\xdb\xe4\x65\x02\x6e\x39\x59\x8d\xbd\x84\xf3\xf9\xfc\x70\x97\x3c\xde\xb3\xcc\xf9\xa5\x65\xc9\x5e\x3e\x9b\x3e\x60\x5f\xc0\x6e\x0d\xca\x39\xcf\x3d\x1c\x1e\xfd\xd6\x30\x5d\x01\xd6\xbb\xb1\xc0\x36\x5c\xaa\x0a\x43\x48\x55\x52\x2f\x57\x42\xbc\x0b\x21\xa6\xd9\x6c\x6b\x45\xe6\x1f\x3d\x92\x0b\xdd\x92\xd6\x68\x93\x40\x53\x70\x46\x0d\x12\x2c\xdb\x69\xb4\x3f\x01\x00\x00\xff\xff\x22\xc0\x9d\x3a\x00\x02\x00\x00")
|
||||||
|
|
||||||
|
func cssMainCssBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_cssMainCss,
|
||||||
|
"css/main.css",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cssMainCss() (*asset, error) {
|
||||||
|
bytes, err := cssMainCssBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "css/main.css", size: 512, mode: os.FileMode(420), modTime: time.Unix(1587564030, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _jsCopyclipboardJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x54\x4f\x6b\xe3\x3e\x10\xbd\xfb\x53\x4c\x75\xb2\xc9\x0f\xf3\x3b\xd7\x78\xa1\x4d\xbb\x10\x28\x4d\x69\xdc\xc3\x9e\x82\x2a\x4d\x1c\xb1\xb2\x14\xa4\x71\xd2\xb2\xf8\xbb\x2f\xb2\xe3\xc4\xf9\xcb\xf6\x50\x1f\x12\x12\x8f\xde\xcc\x7b\x6f\x9e\x58\xed\x11\x3c\x39\x25\x88\x65\x51\x24\xac\xf1\x04\xe3\xe9\xcb\xaf\x79\x31\x9d\x8f\x9f\x26\x2f\xf7\xd3\xbb\xd7\x87\xf9\x78\xfa\x5c\xdc\x4d\x9e\x1f\x5f\xe7\x93\x07\xc8\x81\x09\xbb\xfa\x2c\xec\x58\xab\xd5\xbb\xe5\x4e\x8e\xad\x21\xae\x0c\x3a\x96\x5d\x84\x98\x15\x77\xc5\xdb\xec\xfc\xf9\x19\x71\xaa\x3d\xcb\x22\x8d\x04\xc7\xd8\xe4\x20\x87\xff\xb3\x28\x5a\xd4\x46\x90\xb2\xe6\xb8\x62\x56\x0b\x81\xde\xc7\x09\xfc\x89\x00\x00\x02\x88\x6f\x11\x1f\x35\x56\x90\x83\xb4\xa2\xae\xd0\x50\x5a\x22\x85\xbf\xd0\xd0\xfd\xe7\x44\xc6\x57\x86\x4c\xb2\x16\x6a\x0f\x93\x2a\x63\xd0\x15\xf8\x41\x61\xfe\xb7\xd7\xa7\x30\x85\x42\x79\xc3\x4e\x2a\x85\xe6\xde\x3f\x29\x4f\xa9\xc3\xca\xae\x31\x66\x0b\xae\x34\x3b\x85\xdc\x17\x72\x29\x63\xe6\x3b\x1e\xa1\xb0\xb9\xcc\xf6\x27\x57\x3a\x16\xbc\xf6\xf8\x0d\x7c\xd5\x02\xe2\x9b\x2d\x78\xfb\x15\xc8\xd6\xe6\xb7\xb1\x1b\x03\xe8\x9c\x75\x5b\xba\xa1\x67\xe5\xcb\xde\x4b\x08\x0c\x51\xde\x02\x83\x51\x77\xb0\x2b\x0b\xcb\x60\x35\xa6\xda\x96\x71\xe5\xcb\xeb\xaa\x56\xbe\xfc\x07\x2d\x07\x2a\x5d\x97\xb3\x17\xfd\x8a\x96\x71\xed\xf4\x50\xc5\xf0\xfa\x71\x8d\x86\x26\xf2\x03\x72\x18\x8d\x4e\x77\x31\x8b\xf6\x42\x6d\x94\x91\x76\x93\x1a\xbe\x56\x25\x27\xeb\x52\xd1\x17\xf6\xa0\x9d\x06\xa7\xfe\x31\x61\x6b\x2d\xc1\x58\x02\xde\xd2\x81\xdd\xd1\x9e\x58\x78\x1c\x52\xed\x4c\xf7\xbb\x69\x3f\x2f\xb7\x4c\x37\x4e\x11\x06\x29\x5b\x56\x29\x2d\xd1\xc4\x71\x02\xf9\x8f\xc1\x2c\x61\xec\x33\xf9\xba\xc9\xf3\x03\xee\xc9\x41\xe7\x33\x24\x76\x91\xdb\xce\xf6\x1f\x7c\x57\xa7\x56\xae\xbe\x4d\xe7\x66\xbc\xb3\x73\x17\xf9\x2f\xd8\x31\xdc\x5c\x6e\xce\x3a\x90\x1d\xb0\xd8\xc2\x6a\x2b\x78\x68\x9a\xae\x9c\x25\x2b\xac\x6e\xb9\xb0\x25\xd1\xca\xdf\xb2\x61\x87\xf0\x84\x0e\xa3\x1c\xd8\x2d\x6c\xf0\xdd\x2b\xc2\xd6\xeb\xda\x2b\x53\x42\x7b\x84\xed\xca\x9b\x01\xf5\x2e\x2d\x6d\xd0\x06\x79\x69\x00\xb5\xc7\x23\x0e\xa2\xbf\x73\xbf\x14\xf8\xe1\x45\x9e\x0c\x45\xdf\x82\x9d\x09\xdc\x52\x49\x89\xa6\x5f\xcb\x26\x6a\x92\xe0\xc7\xdf\x00\x00\x00\xff\xff\x70\xbd\x7a\x61\x39\x06\x00\x00")
|
||||||
|
|
||||||
|
func jsCopyclipboardJsBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_jsCopyclipboardJs,
|
||||||
|
"js/copyclipboard.js",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsCopyclipboardJs() (*asset, error) {
|
||||||
|
bytes, err := jsCopyclipboardJsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "js/copyclipboard.js", size: 1593, mode: os.FileMode(420), modTime: time.Unix(1587564030, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _jsDragdropJs = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x95\xcd\x6e\xdb\x46\x10\xc7\xef\x7e\x8a\x01\x7b\xa0\x08\x28\x94\x22\x20\x8e\x11\xd5\x39\xb4\x4e\xd1\x02\x4d\xd3\x83\x8d\x02\xbd\x0d\x77\x47\xe4\xd4\xab\x5d\x62\x77\x24\x86\x28\x72\xeb\x93\xf4\xd1\xfa\x24\xc5\x2e\x3f\x2c\xb7\xb6\x63\x9f\x04\x91\xf3\xf1\x9b\xff\xfc\x97\x9b\x1d\x02\x41\x10\xcf\x4a\xb2\xed\xd9\x59\xc7\x56\xbb\xae\x44\xad\x3f\x1c\xc9\xca\xcf\x1c\x84\x2c\xf9\x45\x7e\xf5\xe9\xe3\xf7\xce\x4a\x7c\xe6\x50\x93\xce\x97\xb0\xa0\x18\x52\xc0\xe5\x7b\xf8\xf3\x0c\x00\x60\xb5\x82\xeb\x86\x80\xf7\xad\xa1\x3d\x59\x41\x61\x67\xc1\xed\x40\x1a\x0e\xa0\x3d\xd6\xaf\xb4\x77\x2d\x18\x57\xb3\x02\x0e\x60\xd0\xd7\x64\x7a\xa8\x30\x90\x06\x67\x41\x1a\x9a\x2a\xa1\x0d\x1d\x79\xd0\x14\x94\xe7\x8a\x34\xb0\x85\x6f\x1b\x91\x36\xbc\x5b\xad\x82\xa0\xba\x75\x47\xf2\x3b\xe3\xba\x52\xb9\xfd\x0a\x57\x9b\x8b\xcd\xe6\x7c\xbd\xd9\xac\xde\x6c\xd6\x6f\xd7\x17\xaf\xdf\x97\x63\xa9\x53\xb6\x9a\x83\xc4\xce\x91\xe8\x1d\xd0\x91\x7c\x0f\xc2\x7b\x8a\x9d\x13\x21\x90\x15\xf2\x01\x10\x2c\x75\x70\xf5\xe9\x23\xb8\xea\x0f\x52\xb2\x04\x16\xe8\xd8\x98\xa9\xda\x8e\x3d\x8d\x51\x79\x4c\x4c\x79\x39\x24\x4d\x86\x51\x50\x80\x06\x21\x4a\x80\xeb\xf8\x97\x63\xdd\x16\xd9\x2e\xa1\x22\x85\x87\x30\x4f\xdb\x11\x68\x07\xd6\x09\xdc\x5a\xd7\x41\xd7\xb0\x6a\xa6\xec\xd4\x16\xaa\x81\xd1\x60\x10\x10\x07\x9e\x14\xf1\x31\x12\xc4\xe6\x86\xf0\x38\xd7\x4a\x08\xcb\xa8\x97\xc2\x30\x64\x1d\x02\x79\xc0\xca\x79\x09\xe9\x7f\xeb\x9d\xa2\x10\x1e\x50\xc8\xd3\xce\x79\x5a\x46\xa0\xe8\x0c\xb4\x70\x53\x9b\x1e\x7e\x44\x75\xfb\xcf\x5f\x7f\xa7\x41\xc8\xd3\x8c\x84\xf6\x84\x52\x1a\x60\x3d\xd5\xca\xe3\xaa\x7f\x77\x96\xf2\xe5\x38\x4e\x65\x9c\xba\x1d\xfa\x2b\x17\x3d\x22\x04\x2d\xd6\x54\x02\xfc\xd6\x90\x05\xfc\xbf\x90\x53\x31\x0e\xd3\xc0\xc9\x26\x68\xfb\xb4\x9a\xb1\x75\xa2\x4d\x44\x07\xdb\xb0\xa6\xc1\x6f\x7b\xa7\xd1\xa4\x06\xaf\x52\x67\xf2\xe5\xdd\x9c\x1c\x86\x84\xfa\x80\x1e\xad\x50\x9c\x76\x58\x59\xc4\xb3\xf4\x59\x20\xbf\xb7\xd1\x69\xe0\x13\x8c\xa9\x5a\xea\x76\xd7\xe7\xde\xd2\x39\x4c\xf9\x04\xa1\x41\x63\x80\x6b\xeb\x3c\x3d\xa0\xbc\x8d\x3a\x91\x8d\x9e\xbc\xdb\x59\x10\xd7\xc6\x7f\xec\x07\x77\xa2\x8a\x67\xea\x6e\xe0\xc9\x06\x27\x07\x27\x9f\x0d\x91\x8f\x47\x0a\xf2\x6f\xe6\x65\x9c\xd0\xfd\x12\x8d\x46\x93\xe1\xc8\x02\x7d\x46\x25\xa6\x9f\x4f\xe0\x69\xcb\xb8\x02\x85\x56\x91\x31\xa4\x61\x81\x56\xcf\x0c\xa3\xe4\x34\x2a\x3e\x36\x98\xab\x28\xe5\xbc\x66\x5b\x9b\xbe\x28\xcf\xd2\x53\x43\x02\x13\x10\x5c\x82\x76\xea\x90\x90\x6a\x92\x0f\x43\xf2\x77\xfd\x4f\x7a\x91\x4d\x31\x59\xb1\x4d\x79\x8f\x7d\x9c\xb2\xd9\x36\xd9\x03\x5f\xa5\x91\xe3\x66\x90\x13\xe3\x11\xb8\x37\xd9\x1c\x34\xb5\x2b\x83\xf4\x86\xca\x23\x07\xae\xd8\xb0\xf4\x70\x09\x59\xb6\x7d\x2c\xce\xb5\xa8\x86\xa0\xd7\x43\xcc\x97\x62\x3b\xcc\x39\x07\x3e\x44\xec\xda\xaf\xc1\xc6\x98\xc8\xba\x63\x43\xf3\xcb\x94\x50\xb6\x3e\xfd\x5e\xd1\x0e\x0f\x46\x16\xc5\x09\xdc\x63\x62\xc6\x2a\x37\xad\x71\xa8\x7f\x60\x32\x3a\x2b\xca\xf8\x24\xc0\xe5\x58\x52\xa3\xe0\xb5\x47\x1b\x76\xe4\x87\x57\x2f\x2b\xea\xfc\x3e\x2b\xca\x70\xa8\xf6\x3c\x03\x3d\x4f\x89\xd1\xad\x5f\x93\x63\x70\x5f\x78\xf9\xea\x1a\xd6\x9a\xec\x73\x16\xb8\xfe\x0f\xf6\x93\x86\x8b\xd7\xcf\x13\xcc\xbf\x0e\x2b\x4a\x07\xa3\xf2\xae\x8b\x23\xec\xbc\xdb\x83\x6b\xc9\xb2\xad\xd3\x8b\xa8\xdf\x7c\x17\xc4\x9b\x12\x13\xdc\xc9\xc7\x6f\xac\x36\x1e\xe4\x11\x08\x60\xf1\xe4\x55\x78\xfe\xf6\xcd\xf9\xf9\xc5\x7a\xbe\x09\x8b\x67\xd9\x27\x8e\xfd\xa5\xd8\xfe\x1b\x00\x00\xff\xff\xdf\xfa\x50\xb3\x12\x08\x00\x00")
|
||||||
|
|
||||||
|
func jsDragdropJsBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_jsDragdropJs,
|
||||||
|
"js/dragdrop.js",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsDragdropJs() (*asset, error) {
|
||||||
|
bytes, err := jsDragdropJsBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "js/dragdrop.js", size: 2066, mode: os.FileMode(420), modTime: time.Unix(1576493365, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesHtmlBaseHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x6c\x90\x31\x6f\x84\x30\x0c\x85\xf7\xfb\x15\xae\x77\xc8\xda\x21\xb0\xb4\x9d\x7b\x43\x97\x8e\x39\xf0\x29\xe8\x0c\x87\x12\x9f\x54\x14\xe5\xbf\x57\x38\x55\xc3\x70\x13\xb6\x1f\x7e\x9f\x5f\xec\xcb\xfb\xe7\xdb\xd7\xf7\xf9\x03\xbc\xcc\xdc\x9f\x6c\xf9\x00\x00\x58\x4f\x6e\x2c\xa5\xb6\x33\x89\x83\xc1\xbb\x10\x49\x3a\x7c\xc8\xb5\x79\xc5\x83\x2c\x93\x30\xf5\x29\x5d\xf8\x3e\xdc\x00\xb5\x45\x68\x73\x0e\x8f\xe8\x79\x5a\x6e\x29\xd1\x32\xe6\x6c\x4d\xf9\xb3\x6e\xee\x22\x04\xe2\x0e\xa3\x6c\x4c\xd1\x13\x09\x82\x6c\x2b\x75\x28\xf4\x23\x66\x88\x11\xc1\x07\xba\x76\xb8\xd7\x66\x76\xd3\xd2\xea\xd0\x54\x9b\x7f\xf2\x7e\x76\xe3\xd6\x95\x96\x51\xf9\xca\x85\x26\xe7\x92\xca\xd4\x58\xf6\x72\x1f\xb7\x27\x0e\xfb\x58\x57\xeb\x8d\x6b\xa0\xfe\x94\xd2\xe0\x98\xa1\x3d\x07\x3a\x6a\x46\xc5\xea\xa2\x31\xff\x60\x85\x60\x8d\x3e\xeb\x6f\x00\x00\x00\xff\xff\x1e\x3c\x4d\x87\x6d\x01\x00\x00")
|
||||||
|
|
||||||
|
func templatesHtmlBaseHtmlTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesHtmlBaseHtmlTmpl,
|
||||||
|
"templates/html/base.html.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesHtmlBaseHtmlTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesHtmlBaseHtmlTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/html/base.html.tmpl", size: 365, mode: os.FileMode(420), modTime: time.Unix(1576604408, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesHtmlErrorHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\x4e\x49\x4d\xcb\xcc\x4b\x55\x50\x2a\xc9\x2c\xc9\x49\x55\xaa\xad\xe5\x72\x2d\x2a\xca\x2f\x52\xd0\x55\x28\x2a\x2d\xce\xc8\xc9\xcc\xcb\xe6\xaa\xae\x4e\xcd\x4b\xa9\xad\x05\x04\x00\x00\xff\xff\x73\xdb\x0d\xaf\x2b\x00\x00\x00")
|
||||||
|
|
||||||
|
func templatesHtmlErrorHtmlTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesHtmlErrorHtmlTmpl,
|
||||||
|
"templates/html/error.html.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesHtmlErrorHtmlTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesHtmlErrorHtmlTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/html/error.html.tmpl", size: 43, mode: os.FileMode(420), modTime: time.Unix(1568580372, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesHtmlIndexHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xbc\x95\x4d\x6f\xe3\x36\x13\xc7\xef\xfc\x14\xb3\xf4\x61\x9f\x07\x88\xc5\x64\x8f\xae\x24\xf4\x0d\x01\x16\x70\x81\xc2\x5b\xa3\x40\x6f\xb4\x38\xb2\x98\x52\x1c\x81\x1c\xd9\x31\xbc\xfe\xee\x05\x29\x3b\x71\x52\x64\xd1\x00\x45\x6f\x1a\x72\xe6\x3f\xe4\x6f\x86\xa3\xe3\xd1\x60\x6b\x3d\x82\xec\x50\x9b\xb9\x1e\x06\xf4\x46\x9e\x4e\xa2\x8c\x4d\xb0\x03\x03\x1f\x06\xac\x24\xe3\x23\xab\x07\xbd\xd3\xd3\xaa\x84\x18\x9a\x4a\xaa\x87\xa8\x4c\xd0\x5b\x13\x68\x28\x1e\xa2\x04\x83\x2d\x86\xba\x54\x93\x57\x2d\x8e\x47\xf4\xe6\x74\x12\xe2\x39\xcd\x86\xcc\x21\xeb\x1b\xbb\x83\xc8\x07\x87\x95\xdc\xd9\x68\x37\xd6\x59\x3e\x2c\x3a\x6b\x0c\xfa\xef\x80\x06\xdd\x24\xfb\x56\x82\x35\x95\x4c\x19\xfe\x20\x8f\xb2\xbe\xb7\x0e\xc1\xfa\x86\x7a\xeb\xb7\x1f\x60\xf1\x73\xa9\x8c\xdd\xd5\xa2\xec\xee\xea\xd9\x6a\x0d\x83\x8e\x8c\x73\x33\xf6\x43\xa9\xba\xbb\x5a\x88\x1f\x75\x44\x03\xe4\xa1\x63\x1e\xe2\x42\xa9\xdb\xc7\xdb\x22\xb2\xba\x01\xee\x6c\x84\x68\x19\x41\x3b\x47\xfb\x08\x07\x1a\x81\x09\x50\x47\xeb\x0e\x30\x0e\x8e\xb4\x81\xd6\x3a\x8c\xa0\xbd\x81\xd8\x51\x60\xf4\xb0\x5e\x2d\x23\x8c\xd1\xfa\xad\xe0\x0e\xa1\xa1\xbe\x4f\xdb\xce\x7a\x2c\x84\x28\xbb\x4f\xf5\xef\xb8\x99\xff\xf0\xeb\xe7\x52\x75\x9f\x6a\x51\x46\x6c\xd8\x92\xaf\x05\x00\x40\xd9\x52\xe8\xf3\x9d\x92\xf0\x3a\xe7\xb8\xa7\xd0\x4b\xd0\xd9\xab\x92\x4a\x42\x8f\xdc\x91\xa9\xe4\x40\x91\x25\xa0\x6f\xa6\x2a\xf4\xa3\x63\x3b\xe8\xc0\x2a\x89\xcc\x8d\x66\x2d\x27\xd5\xac\xec\xf4\x06\x1d\x34\x4e\xc7\x58\xc9\xe4\xb1\x4c\x0b\x12\x5a\x0a\x2f\xb2\x59\x74\x46\xd6\x93\x01\x3a\x5f\x70\x51\xaa\x1c\x7d\xa5\x66\xfd\x30\xf2\xeb\x83\xe6\xd0\xeb\x14\xbf\x68\xeb\x25\x78\xdd\xe3\xe4\x27\xcf\x0d\x33\x7d\xab\x77\xeb\x7d\x19\x37\xbd\xe5\x8b\x4a\x3c\x5b\x3b\xed\x46\xac\xe4\x54\x91\x0f\x4f\xba\x65\xe6\x50\x8b\x52\x3d\x21\x7e\x0b\xf6\xb9\x76\xeb\xd5\xf2\xbf\x80\x7d\x95\xed\x15\xec\xf5\x6a\xf9\x4d\xd6\xaf\x23\xdf\x62\x7d\xf6\xbb\x80\x1a\x83\x7b\x8b\xf6\xb3\xe2\x85\xed\x3f\xc5\x7d\x8e\xfc\x26\xef\xdc\xee\x3f\x5d\xbd\x00\x78\xee\xfb\x21\x60\x2d\x66\xf0\xa2\xd1\x44\x33\x06\x07\xf3\xfb\x8f\xc9\xa8\xbe\x3f\xd0\x18\xd2\x57\x31\xf8\xed\x47\x28\x35\x74\x01\xdb\x4a\x1e\x8f\xc5\x8a\x88\xd7\xab\xe5\xe9\x24\xeb\x6b\xab\x54\xba\x16\x62\x06\x5f\xce\x6f\x31\x13\x7d\x12\x3d\x9f\xb8\x4a\x4f\x7d\xa1\x14\x3e\xea\x7e\x70\x58\x34\xd4\xab\x48\x3d\x2a\x47\x7e\xab\xc6\xe0\xde\x95\xea\xb7\x0e\xa1\xb5\x21\xf2\x74\x3f\x6a\x21\xbd\xfa\x80\x71\x74\x0c\x7b\xeb\x1c\x34\xe4\x59\x5b\x9f\xd7\xcf\x47\x40\x93\x0e\x56\x88\x99\x98\xc1\xe7\x69\x87\xb8\xc3\x90\x35\xe2\x4d\x1e\x35\x39\xb6\xb5\xde\x9c\xb7\xac\x4f\x78\x75\x02\x7b\x93\x46\x9c\x1b\x4d\x9a\x31\xb3\xeb\x8d\x3c\xc8\x68\x9f\xc6\x94\x41\x87\x8c\xaf\x92\xd2\xe6\x01\x1b\x2e\xc4\x33\x77\x1a\x39\xb5\x42\x1b\xa8\x07\xed\xa7\x4c\x43\xa0\x06\x63\x14\xad\xdd\x3a\x64\x58\x8d\xb1\x73\xd6\xff\x09\x5f\xe1\x65\x79\xe6\xef\x03\x45\x97\x99\x39\xd5\x3a\x0f\x4d\xf2\xee\x00\xf8\xc8\x41\x37\xfc\x77\x40\xf0\x3f\x5b\x60\x01\xdc\x05\xda\x83\xde\xeb\xc3\x85\x2d\xff\xff\xdf\xe8\x14\xf8\x0a\xe9\xb7\x06\x73\x0f\x77\xa2\x54\xb9\x21\x2f\xff\xa4\xbf\x02\x00\x00\xff\xff\x55\x95\x0e\x65\xfa\x06\x00\x00")
|
||||||
|
|
||||||
|
func templatesHtmlIndexHtmlTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesHtmlIndexHtmlTmpl,
|
||||||
|
"templates/html/index.html.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesHtmlIndexHtmlTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesHtmlIndexHtmlTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/html/index.html.tmpl", size: 1786, mode: os.FileMode(420), modTime: time.Unix(1576794526, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesHtmlPastemetaHtmlTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x94\xc1\x6e\xdb\x30\x0c\x86\xef\x7a\x0a\x42\x87\x36\x3d\x38\xbe\xb7\xb6\x07\x2c\xdd\x80\x61\x3d\x0c\x5d\x7b\xd9\x4d\xb1\x98\x5a\x8d\x22\x69\x12\xdd\xd6\x10\xf4\xee\x83\xec\x3a\xe9\xb6\x60\x4b\x2f\x01\x42\x51\x1f\xff\x9f\x22\x1d\xa3\xc4\x8d\x32\x08\x9c\x14\x69\xe4\x29\xb1\xf3\x18\x97\xdf\x44\x20\x5c\x7e\xc5\x21\xa5\x18\x97\x9f\x95\xc6\x4f\x2f\x94\xd2\x39\xec\x90\x84\x14\x24\xa0\x00\xdf\x87\x4e\x2b\xb3\x65\x31\xa2\x91\x29\x31\x76\x80\x75\x28\x64\x21\x9c\x43\x23\x33\xb2\x0a\xad\x57\x8e\x80\x06\x87\x35\x27\x7c\xa1\xf2\x51\x3c\x89\x29\xca\x21\xf8\xb6\xe6\xe5\x63\x28\x5b\xeb\x86\x56\x2b\xb7\xb6\xc2\xcb\xe5\x63\xe0\x20\x71\x83\xbe\xa9\xca\x29\xb5\x39\x56\x6b\x6d\xe5\x30\x16\x71\x1e\x41\xc9\x9a\x67\xca\x9d\x5d\xcd\x9c\x95\x35\x24\x94\x41\xcf\xa1\xd5\x22\x84\x9a\x77\x4a\x4a\x34\xbc\x61\xd5\xba\x27\xb2\x06\xac\x69\xb5\x6a\xb7\x7f\x5d\x5d\xe4\x5e\xdc\x5a\x4b\xf7\xb7\x37\x29\x95\xff\x68\xcc\x05\x6f\x56\xd6\x0d\x70\x7f\x7b\x03\x64\x61\x6f\xa2\x2a\xa7\x12\x0d\x54\xc1\x09\x73\x4c\xdf\x77\x12\xd4\x07\x9e\x4d\x3a\x61\x1a\x56\x95\xce\x63\x33\xda\x69\x58\x25\xa0\xf3\xb8\xa9\xf9\x89\x42\x78\x73\x62\x62\x55\x8a\x86\x15\x45\xc1\x62\x54\x1b\x10\x46\xc2\xc2\x20\xbc\x66\x67\x49\xf9\xd7\x2b\xf3\x00\x5c\xa2\x46\x42\xc9\x2f\x60\xb9\x12\xe6\x7a\xfc\xf7\xd1\x5a\x9d\x12\x7b\x56\xd4\xc1\x74\x0e\x64\xb7\x68\x2e\xe1\xbd\x8a\x3f\x4c\xd7\xef\xf2\xed\x7a\x9f\x76\x7d\x08\xbe\xc3\xd4\x09\xac\xd1\x77\x8c\xa8\x03\x42\x71\xdc\xc1\x99\xa6\xab\xde\x6c\x8d\x7d\x36\x67\x0f\x74\x35\xcd\xdc\x98\x9c\xe7\xf7\x12\xf6\xe0\xbb\xc1\x61\x4a\x2c\xe4\x6e\xbd\x09\x8f\xdd\x4b\x69\xea\xec\x9c\xaa\x76\xb8\xf2\x28\x08\xe5\xf2\x4b\xf8\x81\xde\x8e\xc0\x76\x0a\x5d\xc2\x6b\xbd\xb7\xca\xf6\x67\x87\x7a\x07\xc8\x88\x9f\x65\xfd\x2e\x3f\xc6\xc3\x33\x4d\x4f\x98\x97\x63\x63\xfd\x0e\x44\x4b\xca\x9a\xfa\xf5\x45\x4f\xe9\x7c\x5e\xf8\xce\xca\x9a\x3b\x1b\x28\x6f\x8c\x32\xae\x9f\x17\x39\xf4\xeb\x9d\x22\x0e\x46\xec\x70\x86\x72\x78\x12\xba\xc7\x9a\x4f\x1c\x70\x19\xca\x59\x8c\x05\xa8\x0d\x58\x0f\x0b\xfc\xf9\xbf\x31\x5b\x18\x4b\x7f\xcc\xda\xc5\x64\x54\x05\xb1\xd6\x28\x47\xdc\xec\x3e\x6f\x4c\x76\xb7\xdf\x9c\xf9\x1b\xf1\x2b\x00\x00\xff\xff\xf7\x78\x59\x94\xdc\x04\x00\x00")
|
||||||
|
|
||||||
|
func templatesHtmlPastemetaHtmlTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesHtmlPastemetaHtmlTmpl,
|
||||||
|
"templates/html/pasteMeta.html.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesHtmlPastemetaHtmlTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesHtmlPastemetaHtmlTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/html/pasteMeta.html.tmpl", size: 1244, mode: os.FileMode(420), modTime: time.Unix(1589228935, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesTxtBaseTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x01\x00\x00\xff\xff\x00\x00\x00\x00\x00\x00\x00\x00")
|
||||||
|
|
||||||
|
func templatesTxtBaseTxtTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesTxtBaseTxtTmpl,
|
||||||
|
"templates/txt/base.txt.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesTxtBaseTxtTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesTxtBaseTxtTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/txt/base.txt.tmpl", size: 0, mode: os.FileMode(420), modTime: time.Unix(1568569370, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesTxtErrorTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xaa\xae\xd6\xf3\x4d\x2d\x2e\x4e\x4c\x4f\xad\xad\x05\x04\x00\x00\xff\xff\xa1\x68\xe0\xbd\x0c\x00\x00\x00")
|
||||||
|
|
||||||
|
func templatesTxtErrorTxtTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesTxtErrorTxtTmpl,
|
||||||
|
"templates/txt/error.txt.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesTxtErrorTxtTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesTxtErrorTxtTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/txt/error.txt.tmpl", size: 12, mode: os.FileMode(420), modTime: time.Unix(1568575116, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesTxtIndexTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x91\xcf\x6a\xdc\x30\x10\xc6\xef\x7a\x8a\x0f\x7c\x48\x0b\xbb\x76\x7a\x0d\x2c\xb4\x85\xed\x1f\x08\x2d\x78\xe3\x07\x50\xed\xf1\x5a\xed\x58\x63\xa4\x11\x5e\x93\xe4\xdd\x8b\x9c\x6d\xd9\x26\x3d\x44\x27\x49\xc3\xcc\xef\xe3\x37\x45\xdd\xa0\xa9\x6f\x71\xf8\xf2\xbd\xbe\xdb\x7f\xdb\xd7\x66\xf7\xfc\x18\xf3\xd1\x46\xea\x20\x1e\x83\xea\x14\x6f\xaa\xea\xfa\x74\x5d\x46\xad\x36\xd0\xc1\x45\x44\xa7\x04\xcb\x2c\x73\xc4\x22\x09\x2a\x20\x1b\x1d\x2f\x88\x83\x04\x25\x9f\x09\x11\x29\x3a\x7f\x34\x3a\x10\x5a\x19\x47\xeb\x3b\xb0\xf3\x54\x1a\x53\x14\x68\x0e\x1f\x3e\xef\x8d\x29\xd0\x4c\x2c\xb6\x83\x45\xef\x98\x4c\x9b\x02\x63\xfb\xe9\x2a\x3f\x76\xef\x17\x49\x21\xdf\xca\xc9\x1f\xaf\x70\x7f\x5f\xd6\x22\xda\xd4\xb7\x8f\x8f\xb9\xf3\x70\x66\xd9\x4c\xfb\xdb\x79\x4e\xb0\xcb\xc9\x6f\xaa\x8a\x4e\x76\x9c\x98\xca\x56\xc6\x2a\xca\x48\x15\x8b\x3f\x56\x29\xf0\xcb\x79\x77\x03\xa1\x77\x21\xea\x1a\x13\xd2\x23\x47\x0f\x14\x13\x2b\x66\xc7\x8c\x56\xbc\x5a\xe7\xd7\xff\x33\x87\xba\x4c\x2f\x4d\x61\x0a\x7c\x7d\xaa\x88\x0e\x14\xd6\x19\x71\xb3\xea\x59\x7b\x7b\xe7\xbb\x73\xc9\xf9\x5e\xc2\x68\xd5\x89\xdf\xc0\xf9\x96\x53\x97\x45\x15\x97\x85\x55\xbe\xcc\x59\x6d\x47\x4c\x4a\xcf\xa0\xf2\xe3\x27\xb5\x5a\x5e\x18\x94\xa4\x53\x52\xf4\x41\x46\x58\xff\x44\x9a\x82\xb4\x14\xa3\xe9\xdd\x91\x49\x51\xa7\x38\xb0\xf3\xbf\xf0\x80\x7f\x45\x6f\xff\x63\x43\x90\x2e\x57\x83\xbc\x3f\xf1\xbc\x80\x4e\x1a\x6c\xab\x2f\x2d\xe0\x8d\x2b\xa9\x84\x0e\x41\x66\xd8\xd9\x2e\x7f\x04\xea\xdb\x57\x2f\x16\x0f\x18\xc8\x76\xd8\x7a\xbc\x33\xbf\x03\x00\x00\xff\xff\x54\x6e\xbb\xfa\xac\x02\x00\x00")
|
||||||
|
|
||||||
|
func templatesTxtIndexTxtTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesTxtIndexTxtTmpl,
|
||||||
|
"templates/txt/index.txt.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesTxtIndexTxtTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesTxtIndexTxtTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/txt/index.txt.tmpl", size: 684, mode: os.FileMode(420), modTime: time.Unix(1576794536, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _templatesTxtPastemetaTxtTmpl = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x51\xcd\x4a\x03\x31\x10\xbe\xe7\x29\x86\x78\x51\x30\xf1\x5e\xfc\x01\xdb\x15\xc4\x1e\xa4\xae\x17\x4f\x5d\x76\xa7\x36\x74\x4d\xea\x66\x96\x76\x09\xf3\xee\xb2\x49\xdd\x2d\xea\x41\xd0\x4b\x20\x33\xdf\xcc\xf7\x33\x21\xe8\x85\x73\xf4\xbc\x98\x33\x5f\x84\xa0\x1f\x0b\x4f\xa8\x1f\xb0\x63\x0e\x41\xdf\x99\x1a\xb3\x3d\x31\x0b\xa5\x94\x08\xc1\xac\xa0\xb0\x15\x9c\x5a\x84\x03\xf2\x89\x8a\xf8\x36\xc6\xbe\x82\xac\xb0\x46\xc2\x4a\x9e\x81\x9e\x16\x76\x16\x7f\xb7\xce\xd5\xcc\x62\x67\x68\x0d\xa9\x0f\xe4\x36\x68\x27\xf0\x4b\xee\x9b\x34\x95\xf7\x43\x57\x03\x6c\x36\x16\x99\x45\x08\x58\x7b\x04\xf5\x33\xd1\x65\x6b\x37\xd6\xed\xec\x75\x8f\xb3\x55\x0f\xa3\x6e\x8b\x51\x41\xda\x96\x77\x5b\x64\x16\xbe\x77\x73\x54\x8e\xee\xe2\x7a\xb3\xfa\x74\x9c\x9b\x37\x9c\x36\x58\x10\x56\xfa\xde\xbf\x60\xe3\x22\x6d\x99\x4a\x13\x38\x70\x1d\x4b\x1a\x7a\x23\xdf\xb8\x24\xa9\x4f\xaa\xc4\xb7\x80\x86\x18\x53\xc4\xcc\xe2\x6f\x67\x58\x2e\x97\xe2\x04\x72\x37\x24\xb4\x36\xfe\x6b\x0c\xe7\x80\x7b\x2c\x5b\xc2\x89\x28\xdb\xa6\x06\xa5\x1a\x7c\x6f\xd1\x13\xc8\x59\x36\xcf\xf2\x4c\x82\xfc\xbf\xdb\xc9\xa8\x29\x46\xc0\x2c\x3e\x02\x00\x00\xff\xff\x34\xf2\xf4\x10\x90\x02\x00\x00")
|
||||||
|
|
||||||
|
func templatesTxtPastemetaTxtTmplBytes() ([]byte, error) {
|
||||||
|
return bindataRead(
|
||||||
|
_templatesTxtPastemetaTxtTmpl,
|
||||||
|
"templates/txt/pasteMeta.txt.tmpl",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func templatesTxtPastemetaTxtTmpl() (*asset, error) {
|
||||||
|
bytes, err := templatesTxtPastemetaTxtTmplBytes()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
info := bindataFileInfo{name: "templates/txt/pasteMeta.txt.tmpl", size: 656, mode: os.FileMode(420), modTime: time.Unix(1589229127, 0)}
|
||||||
|
a := &asset{bytes: bytes, info: info}
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Asset loads and returns the asset for the given name.
|
||||||
|
// It returns an error if the asset could not be found or
|
||||||
|
// could not be loaded.
|
||||||
|
func Asset(name string) ([]byte, error) {
|
||||||
|
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||||
|
if f, ok := _bindata[cannonicalName]; ok {
|
||||||
|
a, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
|
||||||
|
}
|
||||||
|
return a.bytes, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("Asset %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustAsset is like Asset but panics when Asset would return an error.
|
||||||
|
// It simplifies safe initialization of global variables.
|
||||||
|
func MustAsset(name string) []byte {
|
||||||
|
a, err := Asset(name)
|
||||||
|
if err != nil {
|
||||||
|
panic("asset: Asset(" + name + "): " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetInfo loads and returns the asset info for the given name.
|
||||||
|
// It returns an error if the asset could not be found or
|
||||||
|
// could not be loaded.
|
||||||
|
func AssetInfo(name string) (os.FileInfo, error) {
|
||||||
|
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||||
|
if f, ok := _bindata[cannonicalName]; ok {
|
||||||
|
a, err := f()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
|
||||||
|
}
|
||||||
|
return a.info, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("AssetInfo %s not found", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetNames returns the names of the assets.
|
||||||
|
func AssetNames() []string {
|
||||||
|
names := make([]string, 0, len(_bindata))
|
||||||
|
for name := range _bindata {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
||||||
|
// _bindata is a table, holding each asset generator, mapped to its name.
|
||||||
|
var _bindata = map[string]func() (*asset, error){
|
||||||
|
"css/main.css": cssMainCss,
|
||||||
|
"js/copyclipboard.js": jsCopyclipboardJs,
|
||||||
|
"js/dragdrop.js": jsDragdropJs,
|
||||||
|
"templates/html/base.html.tmpl": templatesHtmlBaseHtmlTmpl,
|
||||||
|
"templates/html/error.html.tmpl": templatesHtmlErrorHtmlTmpl,
|
||||||
|
"templates/html/index.html.tmpl": templatesHtmlIndexHtmlTmpl,
|
||||||
|
"templates/html/pasteMeta.html.tmpl": templatesHtmlPastemetaHtmlTmpl,
|
||||||
|
"templates/txt/base.txt.tmpl": templatesTxtBaseTxtTmpl,
|
||||||
|
"templates/txt/error.txt.tmpl": templatesTxtErrorTxtTmpl,
|
||||||
|
"templates/txt/index.txt.tmpl": templatesTxtIndexTxtTmpl,
|
||||||
|
"templates/txt/pasteMeta.txt.tmpl": templatesTxtPastemetaTxtTmpl,
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssetDir returns the file names below a certain
|
||||||
|
// directory embedded in the file by go-bindata.
|
||||||
|
// For example if you run go-bindata on data/... and data contains the
|
||||||
|
// following hierarchy:
|
||||||
|
// data/
|
||||||
|
// foo.txt
|
||||||
|
// img/
|
||||||
|
// a.png
|
||||||
|
// b.png
|
||||||
|
// then AssetDir("data") would return []string{"foo.txt", "img"}
|
||||||
|
// AssetDir("data/img") would return []string{"a.png", "b.png"}
|
||||||
|
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
|
||||||
|
// AssetDir("") will return []string{"data"}.
|
||||||
|
func AssetDir(name string) ([]string, error) {
|
||||||
|
node := _bintree
|
||||||
|
if len(name) != 0 {
|
||||||
|
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||||
|
pathList := strings.Split(cannonicalName, "/")
|
||||||
|
for _, p := range pathList {
|
||||||
|
node = node.Children[p]
|
||||||
|
if node == nil {
|
||||||
|
return nil, fmt.Errorf("Asset %s not found", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if node.Func != nil {
|
||||||
|
return nil, fmt.Errorf("Asset %s not found", name)
|
||||||
|
}
|
||||||
|
rv := make([]string, 0, len(node.Children))
|
||||||
|
for childName := range node.Children {
|
||||||
|
rv = append(rv, childName)
|
||||||
|
}
|
||||||
|
return rv, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type bintree struct {
|
||||||
|
Func func() (*asset, error)
|
||||||
|
Children map[string]*bintree
|
||||||
|
}
|
||||||
|
|
||||||
|
var _bintree = &bintree{nil, map[string]*bintree{
|
||||||
|
"css": &bintree{nil, map[string]*bintree{
|
||||||
|
"main.css": &bintree{cssMainCss, map[string]*bintree{}},
|
||||||
|
}},
|
||||||
|
"js": &bintree{nil, map[string]*bintree{
|
||||||
|
"copyclipboard.js": &bintree{jsCopyclipboardJs, map[string]*bintree{}},
|
||||||
|
"dragdrop.js": &bintree{jsDragdropJs, map[string]*bintree{}},
|
||||||
|
}},
|
||||||
|
"templates": &bintree{nil, map[string]*bintree{
|
||||||
|
"html": &bintree{nil, map[string]*bintree{
|
||||||
|
"base.html.tmpl": &bintree{templatesHtmlBaseHtmlTmpl, map[string]*bintree{}},
|
||||||
|
"error.html.tmpl": &bintree{templatesHtmlErrorHtmlTmpl, map[string]*bintree{}},
|
||||||
|
"index.html.tmpl": &bintree{templatesHtmlIndexHtmlTmpl, map[string]*bintree{}},
|
||||||
|
"pasteMeta.html.tmpl": &bintree{templatesHtmlPastemetaHtmlTmpl, map[string]*bintree{}},
|
||||||
|
}},
|
||||||
|
"txt": &bintree{nil, map[string]*bintree{
|
||||||
|
"base.txt.tmpl": &bintree{templatesTxtBaseTxtTmpl, map[string]*bintree{}},
|
||||||
|
"error.txt.tmpl": &bintree{templatesTxtErrorTxtTmpl, map[string]*bintree{}},
|
||||||
|
"index.txt.tmpl": &bintree{templatesTxtIndexTxtTmpl, map[string]*bintree{}},
|
||||||
|
"pasteMeta.txt.tmpl": &bintree{templatesTxtPastemetaTxtTmpl, map[string]*bintree{}},
|
||||||
|
}},
|
||||||
|
}},
|
||||||
|
}}
|
||||||
|
|
||||||
|
// RestoreAsset restores an asset under the given directory
|
||||||
|
func RestoreAsset(dir, name string) error {
|
||||||
|
data, err := Asset(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
info, err := AssetInfo(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RestoreAssets restores an asset under the given directory recursively
|
||||||
|
func RestoreAssets(dir, name string) error {
|
||||||
|
children, err := AssetDir(name)
|
||||||
|
// File
|
||||||
|
if err != nil {
|
||||||
|
return RestoreAsset(dir, name)
|
||||||
|
}
|
||||||
|
// Dir
|
||||||
|
for _, child := range children {
|
||||||
|
err = RestoreAssets(dir, filepath.Join(name, child))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func _filePath(dir, name string) string {
|
||||||
|
cannonicalName := strings.Replace(name, "\\", "/", -1)
|
||||||
|
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
|
||||||
|
}
|
||||||
@@ -13,21 +13,22 @@ var (
|
|||||||
fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored")
|
fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored")
|
||||||
httpListen = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)")
|
httpListen = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)")
|
||||||
metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)")
|
metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)")
|
||||||
|
rootURL = flag.String("root_url", "", "host root (example: 'https://example.com', uses an educated guess if omitted)")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
database, err := db.OpenDB(*databasePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
defer database.Close()
|
|
||||||
filestore, err := db.OpenFileStore(*fileStorePath)
|
filestore, err := db.OpenFileStore(*fileStorePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
|
database, err := db.OpenDB(*databasePath, filestore)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(err)
|
||||||
|
}
|
||||||
|
defer database.Close()
|
||||||
|
|
||||||
go rushlink.StartMetricsServer(*metricsListen, database)
|
go rushlink.StartMetricsServer(*metricsListen, database, filestore)
|
||||||
rushlink.StartMainServer(*httpListen, database, filestore)
|
rushlink.StartMainServer(*httpListen, database, filestore, *rootURL)
|
||||||
}
|
}
|
||||||
|
|||||||
122
contrib/rushlink
Executable file
122
contrib/rushlink
Executable file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
LINK=https://hashru.link/
|
||||||
|
DELFILE="${XDG_DATA_HOME:-$HOME/.local/share}/rushlink/delete"
|
||||||
|
|
||||||
|
link() {
|
||||||
|
case "$1" in
|
||||||
|
https://*|http://*)
|
||||||
|
printf '%s' "$1" | curl -sS -F'shorten=<-' "$LINK"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
curl -sS -Ffile=@- "$LINK" < "$1"
|
||||||
|
;;
|
||||||
|
esac | awk 'NR == 1 { print } $NF ~ /deleteToken=/ { print $NF; exit }' | (
|
||||||
|
IFS= read -r link
|
||||||
|
IFS= read -r deletelink
|
||||||
|
echo "$link"
|
||||||
|
mkdir -p "$(dirname "$DELFILE")"
|
||||||
|
echo "$deletelink" >> "$DELFILE"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
del() {
|
||||||
|
case "$1" in
|
||||||
|
https://*|http://*)
|
||||||
|
URL="$1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
URL="$LINK$1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# Remove file extension, if any.
|
||||||
|
NAME="${URL##*/}"
|
||||||
|
URL="${URL%/*}/${NAME%%.*}"
|
||||||
|
if DELURL=$(grep -s -m1 "$URL?deleteToken=" "$DELFILE"); then
|
||||||
|
echo "Deleting $URL..." >&2
|
||||||
|
curl -sS -X DELETE "$DELURL" | grep deleted
|
||||||
|
else
|
||||||
|
echo "Delete token for $URL is not known." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
screenshot() {
|
||||||
|
if command -v import >/dev/null 2>&1; then
|
||||||
|
CMD="import png:-"
|
||||||
|
elif command -v maim >/dev/null 2>&1; then
|
||||||
|
CMD="maim -uks"
|
||||||
|
else
|
||||||
|
echo "Neither import (imagemagick) nor maim were found. One of these is needed to make screenshots." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
FILE=$(mktemp)
|
||||||
|
if $CMD > "$FILE"; then
|
||||||
|
LINK=$(link "$FILE")
|
||||||
|
rm -f "$FILE"
|
||||||
|
echo "$LINK.png"
|
||||||
|
if command -v xdg-open >/dev/null 2>&1; then
|
||||||
|
xdg-open "$LINK.png"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
rm -f "$FILE"
|
||||||
|
exit $?
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
case "$1" in
|
||||||
|
--help|-h)
|
||||||
|
CMD=$(basename "$0")
|
||||||
|
echo "$CMD - Command line tool for $LINK"
|
||||||
|
echo
|
||||||
|
echo "Usage:"
|
||||||
|
echo " $CMD https://example.com/"
|
||||||
|
echo " Shorten link."
|
||||||
|
echo " $CMD file.txt"
|
||||||
|
echo " $CMD < file.txt"
|
||||||
|
echo " Upload file."
|
||||||
|
echo " $CMD"
|
||||||
|
echo " echo hi | $CMD"
|
||||||
|
echo " Upload file from standard input."
|
||||||
|
echo " $CMD (--screenshot|-s)"
|
||||||
|
echo " Select a window or an area of your screen, and upload it as png."
|
||||||
|
echo " $CMD (--delete|-d) ${LINK}xd42"
|
||||||
|
echo " $CMD (--delete|-d) xd42"
|
||||||
|
echo " Delete file or shortened link."
|
||||||
|
echo
|
||||||
|
echo "Delete tokens are stored in $DELFILE"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--delete|-d)
|
||||||
|
shift
|
||||||
|
for url in "$@"; do
|
||||||
|
del "$url"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--screenshot|-s)
|
||||||
|
shift
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
echo "No arguments expected to --screenshot" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
screenshot
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
for url in "$@"; do
|
||||||
|
link "$url"
|
||||||
|
done
|
||||||
|
exit 0
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -t 0 ]; then
|
||||||
|
echo "Sending standard input to $LINK" >&2
|
||||||
|
echo "^C to cancel, ^D to send." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
link /dev/stdin
|
||||||
8
go.mod
8
go.mod
@@ -4,8 +4,8 @@ go 1.12
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.1.1
|
github.com/google/uuid v1.1.1
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.4
|
||||||
github.com/pkg/errors v0.8.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/prometheus/client_golang v1.1.0
|
github.com/prometheus/client_golang v1.5.1
|
||||||
go.etcd.io/bbolt v1.3.3
|
go.etcd.io/bbolt v1.3.4
|
||||||
)
|
)
|
||||||
|
|||||||
37
go.sum
37
go.sum
@@ -1,12 +1,17 @@
|
|||||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
|
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
|
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
@@ -16,16 +21,24 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
|
|||||||
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs=
|
||||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
|
||||||
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
|
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
|
||||||
|
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
|
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
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/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -36,40 +49,64 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
|
|||||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||||
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
|
github.com/prometheus/client_golang v1.1.0 h1:BQ53HtBmfOitExawJ6LokA4x8ov/z0SYYb0+HxJfRI8=
|
||||||
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g=
|
||||||
|
github.com/prometheus/client_golang v1.5.1 h1:bdHYieyGlH+6OLEk2YQha8THib30KP0/yD0YH9m6xcA=
|
||||||
|
github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||||
|
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||||
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
|
github.com/prometheus/common v0.6.0 h1:kRhiuYSXR3+uv2IbVbZhUxK5zVD/2pp3Gd2PpvPkpEo=
|
||||||
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc=
|
||||||
|
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
|
||||||
|
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||||
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
|
github.com/prometheus/procfs v0.0.3 h1:CTwfnzjQ+8dS6MhHHu4YswVAD99sL2wjPqP+VkURmKE=
|
||||||
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ=
|
||||||
|
github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8=
|
||||||
|
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||||
|
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||||
|
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
|
||||||
|
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/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-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
|
||||||
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||||
|
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
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/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
|||||||
252
handlers.go
252
handlers.go
@@ -3,12 +3,12 @@ package rushlink
|
|||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
"crypto/subtle"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.hashru.nl/dsprenkels/rushlink/internal/db"
|
"gitea.hashru.nl/dsprenkels/rushlink/internal/db"
|
||||||
@@ -28,65 +28,55 @@ const (
|
|||||||
|
|
||||||
const cookieDeleteToken = "owner_token"
|
const cookieDeleteToken = "owner_token"
|
||||||
|
|
||||||
func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) {
|
type canDelete uint
|
||||||
render(w, r, "index", map[string]interface{}{})
|
|
||||||
|
const (
|
||||||
|
canDeleteUndef canDelete = iota
|
||||||
|
canDeleteYes
|
||||||
|
canDeleteNo
|
||||||
|
)
|
||||||
|
|
||||||
|
func (cd *canDelete) Bool() bool {
|
||||||
|
return *cd == canDeleteYes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) uploadFileGetHandler(w http.ResponseWriter, r *http.Request) {
|
func (cd *canDelete) String() string {
|
||||||
vars := mux.Vars(r)
|
switch *cd {
|
||||||
id := vars["id"]
|
case canDeleteUndef:
|
||||||
|
return "undefined"
|
||||||
var fu *db.FileUpload
|
case canDeleteYes:
|
||||||
var badID bool
|
return "correct"
|
||||||
if err := rl.db.Bolt.View(func(tx *bolt.Tx) error {
|
case canDeleteNo:
|
||||||
fuID, err := uuid.Parse(id)
|
return "invalid"
|
||||||
if err != nil {
|
default:
|
||||||
badID = true
|
panic("unreachable")
|
||||||
return err
|
|
||||||
}
|
|
||||||
fu, err = db.GetFileUpload(tx, fuID)
|
|
||||||
return err
|
|
||||||
}); err != nil {
|
|
||||||
if badID {
|
|
||||||
renderError(w, r, http.StatusNotFound, "malformed file id")
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
filePath := rl.fs.FilePath(fu.ID, fu.FileName)
|
func (rl *rushlink) staticGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
file, err := os.Open(filePath)
|
rl.renderStatic(w, r, mux.Vars(r)["path"])
|
||||||
if err != nil {
|
}
|
||||||
if os.IsNotExist(err) {
|
|
||||||
log.Printf("error: %v should exist according to the database, but it doesn't", filePath)
|
func (rl *rushlink) indexGetHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
renderError(w, r, http.StatusNotFound, "file not found")
|
rl.render(w, r, http.StatusOK, "index", map[string]interface{}{})
|
||||||
return
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", fu.ContentType)
|
|
||||||
io.Copy(w, file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) {
|
func (rl *rushlink) viewPasteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
rl.viewPasteHandlerInner(w, r, 0)
|
rl.viewPasteHandlerFlags(w, r, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) viewPasteHandlerNoRedirect(w http.ResponseWriter, r *http.Request) {
|
func (rl *rushlink) viewPasteHandlerNoRedirect(w http.ResponseWriter, r *http.Request) {
|
||||||
rl.viewPasteHandlerInner(w, r, viewNoRedirect)
|
rl.viewPasteHandlerFlags(w, r, viewNoRedirect)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) {
|
func (rl *rushlink) viewPasteHandlerMeta(w http.ResponseWriter, r *http.Request) {
|
||||||
rl.viewPasteHandlerInner(w, r, viewShowMeta)
|
rl.viewPasteHandlerFlags(w, r, viewShowMeta)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste) {
|
func (rl *rushlink) viewPasteHandlerFlags(w http.ResponseWriter, r *http.Request, flags viewPaste) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
key := vars["key"]
|
key := vars["key"]
|
||||||
var p *db.Paste
|
var p *db.Paste
|
||||||
var fuID *uuid.UUID
|
|
||||||
var fu *db.FileUpload
|
var fu *db.FileUpload
|
||||||
if err := rl.db.Bolt.View(func(tx *bolt.Tx) error {
|
if err := rl.db.Bolt.View(func(tx *bolt.Tx) error {
|
||||||
var err error
|
var err error
|
||||||
@@ -97,7 +87,6 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
|
|||||||
if p != nil && p.Type == db.PasteTypeFileUpload {
|
if p != nil && p.Type == db.PasteTypeFileUpload {
|
||||||
var id uuid.UUID
|
var id uuid.UUID
|
||||||
copy(id[:], p.Content)
|
copy(id[:], p.Content)
|
||||||
fuID = &id
|
|
||||||
fu, err = db.GetFileUpload(tx, id)
|
fu, err = db.GetFileUpload(tx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -109,62 +98,130 @@ func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p == nil {
|
if p == nil {
|
||||||
renderError(w, r, http.StatusNotFound, "url key not found in the database")
|
rl.renderError(w, r, http.StatusNotFound, "url key not found in the database")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if flags&viewShowMeta != 0 {
|
rl.viewPasteHandlerInner(w, r, flags, p, fu)
|
||||||
canDelete := struct {
|
}
|
||||||
Bool bool
|
|
||||||
String string
|
|
||||||
}{Bool: false}
|
|
||||||
deleteToken := getDeleteTokenFromRequest(r)
|
|
||||||
if deleteToken == "" {
|
|
||||||
canDelete.String = "undefined"
|
|
||||||
} else {
|
|
||||||
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 {
|
|
||||||
canDelete.Bool = true
|
|
||||||
canDelete.String = "correct"
|
|
||||||
} else {
|
|
||||||
canDelete.String = "invalid"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data := map[string]interface{}{
|
func (rl *rushlink) viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPaste, p *db.Paste, fu *db.FileUpload) {
|
||||||
"Paste": p,
|
if flags&viewShowMeta != 0 {
|
||||||
"CanDelete": canDelete,
|
rl.viewPasteHandlerInnerMeta(w, r, p, fu)
|
||||||
}
|
|
||||||
render(w, r, "pasteMeta", data)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch p.State {
|
switch p.State {
|
||||||
case db.PasteStatePresent:
|
case db.PasteStatePresent:
|
||||||
var location string
|
|
||||||
switch p.Type {
|
switch p.Type {
|
||||||
case db.PasteTypeFileUpload:
|
case db.PasteTypeFileUpload:
|
||||||
if fu == nil {
|
if fu == nil {
|
||||||
panic(fmt.Sprintf("file for id %v does not exist in database\n", fuID))
|
panic(fmt.Sprintf("file for id %v does not exist in database\n", string(p.Content)))
|
||||||
}
|
}
|
||||||
location = fu.URL().String()
|
rl.viewFileUploadHandler(w, r, fu)
|
||||||
break
|
return
|
||||||
case db.PasteTypeRedirect:
|
case db.PasteTypeRedirect:
|
||||||
location = p.RedirectURL().String()
|
if flags&viewNoRedirect == 0 {
|
||||||
break
|
http.Redirect(w, r, p.RedirectURL().String(), http.StatusTemporaryRedirect)
|
||||||
|
}
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
panic("paste type unsupported")
|
panic("paste type unsupported")
|
||||||
}
|
}
|
||||||
if flags&viewNoRedirect == 0 {
|
|
||||||
http.Redirect(w, r, location, http.StatusSeeOther)
|
|
||||||
}
|
|
||||||
fmt.Fprint(w, location)
|
|
||||||
case db.PasteStateDeleted:
|
case db.PasteStateDeleted:
|
||||||
renderError(w, r, http.StatusGone, "paste has been deleted\n")
|
rl.renderError(w, r, http.StatusGone, "paste has been deleted\n")
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key))
|
panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rl *rushlink) viewFileUploadHandler(w http.ResponseWriter, r *http.Request, fu *db.FileUpload) {
|
||||||
|
filePath := fu.Path(rl.fs)
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
log.Printf("error: '%v' should exist according to the database, but it doesn't", filePath)
|
||||||
|
rl.renderError(w, r, http.StatusNotFound, "file not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// unexpected error
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var modtime time.Time
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error: %v", errors.Wrapf(err, "could not stat file '%v'", filePath))
|
||||||
|
} else {
|
||||||
|
modtime = info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provide the real filename to the client (to be used in Ctrl+S etc.)
|
||||||
|
quotedName := strings.ReplaceAll(fu.FileName, "\"", "\\\"")
|
||||||
|
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", quotedName))
|
||||||
|
|
||||||
|
// We use http.ServeContent (instead of http.ServeFile) because we cannot
|
||||||
|
// use http.ServeFile together with the assertion that the file exists,
|
||||||
|
// without introducing a TOCTOU flaw.
|
||||||
|
http.ServeContent(w, r, fu.FileName, modtime, file)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *rushlink) viewPasteHandlerInnerMeta(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
|
||||||
|
var cd canDelete
|
||||||
|
deleteToken := getDeleteTokenFromRequest(r)
|
||||||
|
if deleteToken != "" {
|
||||||
|
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 {
|
||||||
|
cd = canDeleteYes
|
||||||
|
} else {
|
||||||
|
cd = canDeleteNo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileExt string
|
||||||
|
if fu != nil {
|
||||||
|
fileExt = fu.Ext()
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Paste": p,
|
||||||
|
"FileExt": fileExt,
|
||||||
|
"CanDeleteString": cd.String(),
|
||||||
|
"CanDeleteBool": cd.Bool(),
|
||||||
|
}
|
||||||
|
var status int
|
||||||
|
if p.State == db.PasteStateDeleted {
|
||||||
|
status = http.StatusGone
|
||||||
|
} else {
|
||||||
|
status = http.StatusOK
|
||||||
|
}
|
||||||
|
rl.render(w, r, status, "pasteMeta", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *rushlink) viewActionSuccess(w http.ResponseWriter, r *http.Request, p *db.Paste, fu *db.FileUpload) {
|
||||||
|
var fileExt string
|
||||||
|
if fu != nil {
|
||||||
|
fileExt = fu.Ext()
|
||||||
|
}
|
||||||
|
// Redirect to the new paste.
|
||||||
|
pasteURL := url.URL{
|
||||||
|
Path: fmt.Sprintf("/%s%s/meta", p.Key, fileExt),
|
||||||
|
RawQuery: fmt.Sprintf("deleteToken=%s", url.QueryEscape(p.DeleteToken)),
|
||||||
|
}
|
||||||
|
http.Redirect(w, r, pasteURL.String(), http.StatusFound)
|
||||||
|
// But still render the page for CURL-like clients.
|
||||||
|
cd := canDeleteYes
|
||||||
|
data := map[string]interface{}{
|
||||||
|
"Paste": p,
|
||||||
|
"FileExt": fileExt,
|
||||||
|
"CanDeleteString": cd.String(),
|
||||||
|
"CanDeleteBool": cd.Bool(),
|
||||||
|
}
|
||||||
|
rl.render(w, r, 0, "pasteMeta", data)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
file, fileHeader, err := r.FormFile("file")
|
file, fileHeader, err := r.FormFile("file")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -174,7 +231,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Fallthrough
|
// Fallthrough
|
||||||
} else {
|
} else {
|
||||||
msg := fmt.Sprintf("could not parse form: %v\n", err)
|
msg := fmt.Sprintf("could not parse form: %v\n", err)
|
||||||
renderError(w, r, http.StatusBadRequest, msg)
|
rl.renderError(w, r, http.StatusBadRequest, msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +241,7 @@ func (rl *rushlink) newPasteHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n")
|
rl.renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
|
func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) {
|
||||||
@@ -192,8 +249,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
|
|||||||
var paste *db.Paste
|
var paste *db.Paste
|
||||||
if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
|
if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
|
||||||
var err error
|
var err error
|
||||||
// Create the fileUpload in the database
|
fu, err = db.NewFileUpload(rl.fs, file, header.Filename)
|
||||||
fu, err = db.NewFileUpload(rl.fs, file, header.Filename, header.Header.Get("Content-Type"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(errors.Wrap(err, "creating fileUpload"))
|
panic(errors.Wrap(err, "creating fileUpload"))
|
||||||
}
|
}
|
||||||
@@ -206,8 +262,7 @@ func (rl *rushlink) newFileUploadPasteHandler(w http.ResponseWriter, r *http.Req
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{"Paste": paste}
|
rl.viewActionSuccess(w, r, paste, fu)
|
||||||
render(w, r, "newFileUploadPasteSuccess", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||||
@@ -217,25 +272,25 @@ func (rl *rushlink) newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Req
|
|||||||
}
|
}
|
||||||
shorten := r.PostFormValue("shorten")
|
shorten := r.PostFormValue("shorten")
|
||||||
if shorten == "" {
|
if shorten == "" {
|
||||||
renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n")
|
rl.renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rl.newRedirectPasteHandler(w, r, shorten)
|
rl.newRedirectPasteHandler(w, r, shorten)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) {
|
func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) {
|
||||||
userURL, err := url.ParseRequestURI(rawurl)
|
userURL, err := url.Parse(rawurl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
|
msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl)
|
||||||
renderError(w, r, http.StatusBadRequest, msg)
|
rl.renderError(w, r, http.StatusBadRequest, msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if userURL.Scheme == "" {
|
if userURL.Scheme == "" {
|
||||||
renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n")
|
rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if userURL.Host == "" {
|
if userURL.Host == "" {
|
||||||
renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n")
|
rl.renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,8 +302,7 @@ func (rl *rushlink) newRedirectPasteHandler(w http.ResponseWriter, r *http.Reque
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
data := map[string]interface{}{"Paste": paste}
|
rl.viewActionSuccess(w, r, paste, nil)
|
||||||
render(w, r, "newRedirectPasteSuccess", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete a URL from the database
|
// Delete a URL from the database
|
||||||
@@ -258,40 +312,38 @@ func (rl *rushlink) deletePasteHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
deleteToken := getDeleteTokenFromRequest(r)
|
deleteToken := getDeleteTokenFromRequest(r)
|
||||||
if deleteToken == "" {
|
if deleteToken == "" {
|
||||||
renderError(w, r, http.StatusBadRequest, "no delete token provided\n")
|
rl.renderError(w, r, http.StatusBadRequest, "no delete token provided\n")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var errorCode int
|
var errorCode int
|
||||||
var paste db.Paste
|
var paste *db.Paste
|
||||||
if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
|
if err := rl.db.Bolt.Update(func(tx *bolt.Tx) error {
|
||||||
p, err := db.GetPaste(tx, key)
|
var err error
|
||||||
|
paste, err = db.GetPaste(tx, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errorCode = http.StatusNotFound
|
errorCode = http.StatusNotFound
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if p.State == db.PasteStateDeleted {
|
if paste.State == db.PasteStateDeleted {
|
||||||
errorCode = http.StatusGone
|
errorCode = http.StatusGone
|
||||||
return errors.New("already deleted")
|
return errors.New("already deleted")
|
||||||
}
|
}
|
||||||
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 0 {
|
if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(paste.DeleteToken)) == 0 {
|
||||||
errorCode = http.StatusForbidden
|
errorCode = http.StatusForbidden
|
||||||
return errors.New("invalid delete token")
|
return errors.New("invalid delete token")
|
||||||
}
|
}
|
||||||
if err := p.Delete(tx, rl.fs); err != nil {
|
if err := paste.Delete(tx, rl.fs); err != nil {
|
||||||
errorCode = http.StatusInternalServerError
|
errorCode = http.StatusInternalServerError
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
paste = *p
|
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
log.Printf("error: %v\n", err)
|
||||||
renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err))
|
rl.renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
rl.viewActionSuccess(w, r, paste, nil)
|
||||||
data := map[string]interface{}{"Paste": paste}
|
|
||||||
render(w, r, "deletePasteSuccess", data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new fileUpload redirect to the database
|
// Add a new fileUpload redirect to the database
|
||||||
|
|||||||
149
handlers_test.go
Normal file
149
handlers_test.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package rushlink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.hashru.nl/dsprenkels/rushlink/internal/db"
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"go.etcd.io/bbolt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createTemporaryRouter initializes a rushlink instance, with temporary
|
||||||
|
// filestore and database.
|
||||||
|
//
|
||||||
|
// It will use testing.T.Cleanup to cleanup after itself.
|
||||||
|
func createTemporaryRouter(t *testing.T) (*mux.Router, *rushlink) {
|
||||||
|
tempDir, err := ioutil.TempDir("", "rushlink-tmp-*")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating temporary directory: %s\n", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
os.RemoveAll(tempDir)
|
||||||
|
})
|
||||||
|
|
||||||
|
fileStore, err := db.OpenFileStore(filepath.Join(tempDir, "filestore"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("opening temporary filestore: %s\n", err)
|
||||||
|
}
|
||||||
|
databasePath := filepath.Join(tempDir, "rushlink.db")
|
||||||
|
database, err := db.OpenDB(databasePath, fileStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("opening temporary database: %s\n", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() {
|
||||||
|
if err := database.Close(); err != nil {
|
||||||
|
t.Errorf("closing database: %d\n", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// *.invalid. is guaranteed not to exist (RFC 6761).
|
||||||
|
rootURL, err := url.Parse("https://rushlink.invalid")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parsing URL: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rl := rushlink{
|
||||||
|
db: database,
|
||||||
|
fs: fileStore,
|
||||||
|
rootURL: rootURL,
|
||||||
|
}
|
||||||
|
return CreateMainRouter(&rl), &rl
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkStatusCode checks whether the status code from a recorded response is equal
|
||||||
|
// to some response code.
|
||||||
|
func checkStatusCode(t *testing.T, rr *httptest.ResponseRecorder, code int) {
|
||||||
|
if actual := rr.Code; actual != code {
|
||||||
|
t.Logf("request body:\n%v\n", rr.Body.String())
|
||||||
|
t.Fatalf("handler returned wrong status code: got %v want %v\n",
|
||||||
|
actual, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkLocationHeader checks whether the status code from a recorded response is equal
|
||||||
|
// to some expected URL.
|
||||||
|
func checkLocationHeader(t *testing.T, rr *httptest.ResponseRecorder, expected string) {
|
||||||
|
location := rr.Header().Get("Location")
|
||||||
|
if location != expected {
|
||||||
|
t.Fatalf("handler returned bad redirect location: got %v want %v", location, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssue43(t *testing.T) {
|
||||||
|
srv, _ := createTemporaryRouter(t)
|
||||||
|
|
||||||
|
// Put a URL with a fragment identifier into the database.
|
||||||
|
var body bytes.Buffer
|
||||||
|
form := multipart.NewWriter(&body)
|
||||||
|
form.WriteField("shorten", "https://example.com#fragment")
|
||||||
|
form.Close()
|
||||||
|
req, err := http.NewRequest("POST", "/", bytes.NewReader(body.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", form.FormDataContentType())
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
checkStatusCode(t, rr, http.StatusFound)
|
||||||
|
rawURL := strings.SplitN(rr.Body.String(), "\n", 2)[0]
|
||||||
|
pasteURL, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the URL was encoded correctly.
|
||||||
|
req, err = http.NewRequest("GET", pasteURL.Path, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req = mux.SetURLVars(req, map[string]string{"key": pasteURL.Path[1:]})
|
||||||
|
rr = httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
checkStatusCode(t, rr, http.StatusTemporaryRedirect)
|
||||||
|
checkLocationHeader(t, rr, "https://example.com#fragment")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIssue53(t *testing.T) {
|
||||||
|
srv, rl := createTemporaryRouter(t)
|
||||||
|
|
||||||
|
// Put a URL with a fragment identifier into the database.
|
||||||
|
var body bytes.Buffer
|
||||||
|
form := multipart.NewWriter(&body)
|
||||||
|
if _, err := form.CreateFormFile("file", "../directory-traversal/file.txt"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
form.Close()
|
||||||
|
req, err := http.NewRequest("POST", "/", bytes.NewReader(body.Bytes()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", form.FormDataContentType())
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
srv.ServeHTTP(rr, req)
|
||||||
|
checkStatusCode(t, rr, http.StatusFound)
|
||||||
|
|
||||||
|
// Check that any attempt to do directory traversal has failed.
|
||||||
|
rl.db.Bolt.View(func(tx *bbolt.Tx) error {
|
||||||
|
fus, err := db.AllFileUploads(tx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, fu := range fus {
|
||||||
|
if strings.ContainsAny(fu.FileName, "/\\") {
|
||||||
|
t.Fatalf(fmt.Sprintf("found a slash in file name: %v", fu.FileName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -24,7 +28,7 @@ type Database struct {
|
|||||||
//
|
//
|
||||||
// If we alter the database format, we bump this number and write a new
|
// If we alter the database format, we bump this number and write a new
|
||||||
// database migration in migrate().
|
// database migration in migrate().
|
||||||
const CurrentMigrateVersion = 2
|
const CurrentMigrateVersion = 3
|
||||||
|
|
||||||
// BucketConf holds the name for the "configuration" bucket.
|
// BucketConf holds the name for the "configuration" bucket.
|
||||||
//
|
//
|
||||||
@@ -42,7 +46,7 @@ const BucketFileUpload = "fileUpload"
|
|||||||
const KeyMigrateVersion = "migrate_version"
|
const KeyMigrateVersion = "migrate_version"
|
||||||
|
|
||||||
// OpenDB opens a database file located at path.
|
// OpenDB opens a database file located at path.
|
||||||
func OpenDB(path string) (*Database, error) {
|
func OpenDB(path string, fs *FileStore) (*Database, error) {
|
||||||
if path == "" {
|
if path == "" {
|
||||||
return nil, errors.New("database not set")
|
return nil, errors.New("database not set")
|
||||||
}
|
}
|
||||||
@@ -51,7 +55,7 @@ func OpenDB(path string) (*Database, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to open database at '%v'", path)
|
return nil, errors.Wrapf(err, "failed to open database at '%v'", path)
|
||||||
}
|
}
|
||||||
if err := db.Update(migrate); err != nil {
|
if err := db.Update(func(tx *bolt.Tx) error { return migrate(tx, fs) }); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &Database{db}, nil
|
return &Database{db}, nil
|
||||||
@@ -62,11 +66,16 @@ func (db *Database) Close() error {
|
|||||||
if db == nil {
|
if db == nil {
|
||||||
panic("no open database")
|
panic("no open database")
|
||||||
}
|
}
|
||||||
return db.Close()
|
return db.Bolt.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize and migrate the database to the current version
|
// Initialize and migrate the database to the current version
|
||||||
func migrate(tx *bolt.Tx) error {
|
func migrate(tx *bolt.Tx, fs *FileStore) error {
|
||||||
|
// Guidelines for error handling:
|
||||||
|
// - Errors based on malformed *structure* should be fatal!
|
||||||
|
// - Errors based on malformed *data* should print a warning
|
||||||
|
// (and if possible try to fix the error).
|
||||||
|
|
||||||
dbVersion, err := dbVersion(tx)
|
dbVersion, err := dbVersion(tx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -104,6 +113,54 @@ func migrate(tx *bolt.Tx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if dbVersion < 3 {
|
||||||
|
log.Println("migrating database to version 3")
|
||||||
|
// In this version, we changed te way how Content-Types are being
|
||||||
|
// stored. Previously, we allowed clients to provide their own
|
||||||
|
// Content-Types for files, using the Content-Disposition header in
|
||||||
|
// multipart forms. The new way detects these types using
|
||||||
|
// http.DetectContentType.
|
||||||
|
//
|
||||||
|
// Scan through all the FileUploads and update their ContentTypes.
|
||||||
|
bucket := tx.Bucket([]byte(BucketFileUpload))
|
||||||
|
cursor := bucket.Cursor()
|
||||||
|
var id, storedBytes []byte
|
||||||
|
id, storedBytes = cursor.First()
|
||||||
|
for id != nil {
|
||||||
|
fu, err := decodeFileUpload(storedBytes)
|
||||||
|
if err != nil {
|
||||||
|
log.Print("error: ", errors.Wrapf(err, "corrupted FileUpload in database at '%v'", id))
|
||||||
|
id, storedBytes = cursor.Next()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if fu.State != FileUploadStatePresent {
|
||||||
|
id, storedBytes = cursor.Next()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filePath := fu.Path(fs)
|
||||||
|
file, err := os.Open(fu.Path(fs))
|
||||||
|
if err != nil {
|
||||||
|
log.Print("error: ", errors.Wrapf(err, "could not open file at '%v'", filePath))
|
||||||
|
id, storedBytes = cursor.Next()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
buf.Grow(512)
|
||||||
|
io.CopyN(&buf, file, 512)
|
||||||
|
contentType := http.DetectContentType(buf.Bytes())
|
||||||
|
if contentType != fu.ContentType {
|
||||||
|
fu.ContentType = contentType
|
||||||
|
fu.Save(tx)
|
||||||
|
cursor.Seek(id)
|
||||||
|
}
|
||||||
|
id, storedBytes = cursor.Next()
|
||||||
|
}
|
||||||
|
// Update the version number
|
||||||
|
if err := setDBVersion(tx, 3); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"hash/crc32"
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -28,11 +31,23 @@ type FileUploadState int
|
|||||||
|
|
||||||
// FileUpload models an uploaded file.
|
// FileUpload models an uploaded file.
|
||||||
type FileUpload struct {
|
type FileUpload struct {
|
||||||
State FileUploadState
|
// State of the FileUpload (present/deleted/etc).
|
||||||
ID uuid.UUID
|
State FileUploadState
|
||||||
FileName string
|
|
||||||
|
// ID identifies this FileUpload.
|
||||||
|
ID uuid.UUID
|
||||||
|
|
||||||
|
// FileName contains the original filename of this FileUpload.
|
||||||
|
FileName string
|
||||||
|
|
||||||
|
// Content type as determined by http.DetectContentType.
|
||||||
ContentType string
|
ContentType string
|
||||||
Checksum uint32
|
|
||||||
|
// Checksum holds a crc32c checksum of the file.
|
||||||
|
//
|
||||||
|
// This checksum is only meant to allow for the detection of random
|
||||||
|
// database corruption.
|
||||||
|
Checksum uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -76,14 +91,43 @@ func OpenFileStore(path string) (*FileStore, error) {
|
|||||||
return &FileStore{path[:]}, nil
|
return &FileStore{path[:]}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path returns the path of the FileStore root.
|
||||||
|
func (fs *FileStore) Path() string {
|
||||||
|
return fs.path
|
||||||
|
}
|
||||||
|
|
||||||
|
// filePath resolves the path of a file in the FileStore given some id and filename.
|
||||||
|
func (fs *FileStore) filePath(id uuid.UUID, fileName string) string {
|
||||||
|
if fs.path == "" {
|
||||||
|
panic("fileStoreDir called while the file store path has not been set")
|
||||||
|
}
|
||||||
|
return path.Join(fs.path, hex.EncodeToString(id[:]), fileName)
|
||||||
|
}
|
||||||
|
|
||||||
// NewFileUpload creates a new FileUpload object.
|
// NewFileUpload creates a new FileUpload object.
|
||||||
func NewFileUpload(fs *FileStore, r io.Reader, fileName string, contentType string) (*FileUpload, error) {
|
//
|
||||||
|
// Internally, this function detects the type of the file stored in `r` using
|
||||||
|
// `http.DetectContentType`.
|
||||||
|
func NewFileUpload(fs *FileStore, r io.Reader, fileName string) (*FileUpload, error) {
|
||||||
|
// Generate a file ID
|
||||||
id, err := uuid.NewRandom()
|
id, err := uuid.NewRandom()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "generating UUID")
|
return nil, errors.Wrap(err, "generating UUID")
|
||||||
}
|
}
|
||||||
|
|
||||||
filePath := fs.FilePath(id, fileName)
|
// Construct a checksum for this file
|
||||||
|
hash := crc32.New(checksumTable)
|
||||||
|
tee := io.TeeReader(r, hash)
|
||||||
|
|
||||||
|
// Detect the file type
|
||||||
|
var tmpBuf bytes.Buffer
|
||||||
|
tmpBuf.Grow(512)
|
||||||
|
io.CopyN(&tmpBuf, tee, 512)
|
||||||
|
contentType := http.DetectContentType(tmpBuf.Bytes())
|
||||||
|
|
||||||
|
// Open the file on disk for writing
|
||||||
|
baseName := filepath.Base(fileName)
|
||||||
|
filePath := fs.filePath(id, baseName)
|
||||||
if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil {
|
if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil {
|
||||||
return nil, errors.Wrap(err, "creating file dir")
|
return nil, errors.Wrap(err, "creating file dir")
|
||||||
}
|
}
|
||||||
@@ -93,8 +137,11 @@ func NewFileUpload(fs *FileStore, r io.Reader, fileName string, contentType stri
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
hash := crc32.New(checksumTable)
|
// Write the file to disk
|
||||||
tee := io.TeeReader(r, hash)
|
_, err = io.Copy(file, &tmpBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "writing to file")
|
||||||
|
}
|
||||||
_, err = io.Copy(file, tee)
|
_, err = io.Copy(file, tee)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "writing to file")
|
return nil, errors.Wrap(err, "writing to file")
|
||||||
@@ -103,24 +150,14 @@ func NewFileUpload(fs *FileStore, r io.Reader, fileName string, contentType stri
|
|||||||
fu := &FileUpload{
|
fu := &FileUpload{
|
||||||
State: FileUploadStatePresent,
|
State: FileUploadStatePresent,
|
||||||
ID: id,
|
ID: id,
|
||||||
FileName: fileName,
|
FileName: baseName,
|
||||||
ContentType: contentType,
|
ContentType: contentType,
|
||||||
Checksum: hash.Sum32(),
|
Checksum: hash.Sum32(),
|
||||||
}
|
}
|
||||||
return fu, nil
|
return fu, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs *FileStore) Path() string {
|
// GetFileUpload tries to retrieve a FileUpload object from the bolt database.
|
||||||
return fs.path
|
|
||||||
}
|
|
||||||
|
|
||||||
func (fs *FileStore) FilePath(id uuid.UUID, fileName string) string {
|
|
||||||
if fs.path == "" {
|
|
||||||
panic("fileStoreDir called while the file store path has not been set")
|
|
||||||
}
|
|
||||||
return path.Join(fs.path, hex.EncodeToString(id[:]), fileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetFileUpload(tx *bolt.Tx, id uuid.UUID) (*FileUpload, error) {
|
func GetFileUpload(tx *bolt.Tx, id uuid.UUID) (*FileUpload, error) {
|
||||||
bucket := tx.Bucket([]byte(BucketFileUpload))
|
bucket := tx.Bucket([]byte(BucketFileUpload))
|
||||||
if bucket == nil {
|
if bucket == nil {
|
||||||
@@ -130,6 +167,31 @@ func GetFileUpload(tx *bolt.Tx, id uuid.UUID) (*FileUpload, error) {
|
|||||||
if storedBytes == nil {
|
if storedBytes == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
return decodeFileUpload(storedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllFileUploads tries to retrieve all FileUpload objects from the bolt database.
|
||||||
|
func AllFileUploads(tx *bolt.Tx) ([]FileUpload, error) {
|
||||||
|
bucket := tx.Bucket([]byte(BucketFileUpload))
|
||||||
|
if bucket == nil {
|
||||||
|
return nil, errors.Errorf("bucket %v does not exist", BucketFileUpload)
|
||||||
|
}
|
||||||
|
var fus []FileUpload
|
||||||
|
err := bucket.ForEach(func(_, storedBytes []byte) error {
|
||||||
|
fu, err := decodeFileUpload(storedBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fus = append(fus, *fu)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeFileUpload(storedBytes []byte) (*FileUpload, error) {
|
||||||
fu := &FileUpload{}
|
fu := &FileUpload{}
|
||||||
err := gobmarsh.Unmarshal(storedBytes, fu)
|
err := gobmarsh.Unmarshal(storedBytes, fu)
|
||||||
return fu, err
|
return fu, err
|
||||||
@@ -155,7 +217,7 @@ func (fu *FileUpload) Save(tx *bolt.Tx) error {
|
|||||||
// Delete deletes a FileUpload from the database.
|
// Delete deletes a FileUpload from the database.
|
||||||
func (fu *FileUpload) Delete(tx *bolt.Tx, fs *FileStore) error {
|
func (fu *FileUpload) Delete(tx *bolt.Tx, fs *FileStore) error {
|
||||||
// Remove the file in the backend
|
// Remove the file in the backend
|
||||||
filePath := fs.FilePath(fu.ID, fu.FileName)
|
filePath := fu.Path(fs)
|
||||||
if err := os.Remove(filePath); err != nil {
|
if err := os.Remove(filePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -173,6 +235,11 @@ func (fu *FileUpload) Delete(tx *bolt.Tx, fs *FileStore) error {
|
|||||||
return errors.Wrap(os.Remove(path.Dir(filePath)), wrap)
|
return errors.Wrap(os.Remove(path.Dir(filePath)), wrap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Path returns the path to this FileUpload in the FileStore provided in fs.
|
||||||
|
func (fu *FileUpload) Path(fs *FileStore) string {
|
||||||
|
return fs.filePath(fu.ID, fu.FileName)
|
||||||
|
}
|
||||||
|
|
||||||
// URL returns the URL for the FileUpload.
|
// URL returns the URL for the FileUpload.
|
||||||
func (fu *FileUpload) URL() *url.URL {
|
func (fu *FileUpload) URL() *url.URL {
|
||||||
rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName
|
rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName
|
||||||
@@ -182,3 +249,8 @@ func (fu *FileUpload) URL() *url.URL {
|
|||||||
}
|
}
|
||||||
return urlParse
|
return urlParse
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ext returns the extension of the file attached to this FileUpload.
|
||||||
|
func (fu *FileUpload) Ext() string {
|
||||||
|
return filepath.Ext(fu.FileName)
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ import (
|
|||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PasteType describes the type of Paste (i.e. file, redirect, [...]).
|
||||||
type PasteType int
|
type PasteType int
|
||||||
|
|
||||||
|
// PasteState describes the state of a Paste (i.e. present, deleted, [...]).
|
||||||
type PasteState int
|
type PasteState int
|
||||||
|
|
||||||
|
// Paste describes the main Paste model in the database.
|
||||||
type Paste struct {
|
type Paste struct {
|
||||||
Type PasteType
|
Type PasteType
|
||||||
State PasteState
|
State PasteState
|
||||||
@@ -35,6 +39,8 @@ var ReservedPasteKeys = []string{"xd42", "example"}
|
|||||||
// allowed at the bottom of this block, for the same reason.
|
// allowed at the bottom of this block, for the same reason.
|
||||||
const (
|
const (
|
||||||
PasteTypeUndef PasteType = iota
|
PasteTypeUndef PasteType = iota
|
||||||
|
// PasteTypePaste is as of yet unused. It is still unclear if this type
|
||||||
|
// will ever get a proper meaning.
|
||||||
PasteTypePaste
|
PasteTypePaste
|
||||||
PasteTypeRedirect
|
PasteTypeRedirect
|
||||||
PasteTypeFileUpload
|
PasteTypeFileUpload
|
||||||
@@ -89,11 +95,16 @@ func GetPaste(tx *bolt.Tx, key string) (*Paste, error) {
|
|||||||
if storedBytes == nil {
|
if storedBytes == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
return decodePaste(storedBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodePaste(storedBytes []byte) (*Paste, error) {
|
||||||
p := &Paste{}
|
p := &Paste{}
|
||||||
err := gobmarsh.Unmarshal(storedBytes, p)
|
err := gobmarsh.Unmarshal(storedBytes, p)
|
||||||
return p, err
|
return p, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save saves this Paste to the database.
|
||||||
func (p *Paste) Save(tx *bolt.Tx) error {
|
func (p *Paste) Save(tx *bolt.Tx) error {
|
||||||
bucket := tx.Bucket([]byte(BucketPastes))
|
bucket := tx.Bucket([]byte(BucketPastes))
|
||||||
if bucket == nil {
|
if bucket == nil {
|
||||||
@@ -110,6 +121,7 @@ func (p *Paste) Save(tx *bolt.Tx) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete deletes this Paste from the database.
|
||||||
func (p *Paste) Delete(tx *bolt.Tx, fs *FileStore) error {
|
func (p *Paste) Delete(tx *bolt.Tx, fs *FileStore) error {
|
||||||
// Remove the (maybe) attached file
|
// Remove the (maybe) attached file
|
||||||
if p.Type == PasteTypeFileUpload {
|
if p.Type == PasteTypeFileUpload {
|
||||||
@@ -226,6 +238,7 @@ func generatePasteKeyInner(epoch int) (string, error) {
|
|||||||
return string(urlKey), nil
|
return string(urlKey), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateDeleteToken generates a new (random) delete token.
|
||||||
func GenerateDeleteToken() (string, error) {
|
func GenerateDeleteToken() (string, error) {
|
||||||
var deleteToken [16]byte
|
var deleteToken [16]byte
|
||||||
_, err := rand.Read(deleteToken[:])
|
_, err := rand.Read(deleteToken[:])
|
||||||
|
|||||||
59
metrics.go
59
metrics.go
@@ -10,34 +10,45 @@ import (
|
|||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
bolt "go.etcd.io/bbolt"
|
bolt "go.etcd.io/bbolt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func StartMetricsServer(addr string, db *db.Database) {
|
const metricNamespace = "rushlink"
|
||||||
var (
|
|
||||||
_ = promauto.NewGaugeFunc(prometheus.GaugeOpts{
|
var metricRequestsTotalCounter = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
Namespace: "rushlink",
|
Namespace: metricNamespace,
|
||||||
Subsystem: "pastes",
|
Subsystem: "http",
|
||||||
Name: "urls_total",
|
Name: "requests_total",
|
||||||
Help: "The current amount of pastes in the database.",
|
Help: "How many HTTP requests processed, partitioned by status code and HTTP method.",
|
||||||
}, func() float64 {
|
}, []string{"code", "method"})
|
||||||
var metric float64
|
|
||||||
if err := db.Bolt.View(func(tx *bolt.Tx) error {
|
func metricURLsTotal(database *db.Database) float64 {
|
||||||
bucket := tx.Bucket([]byte("pastes"))
|
var metric float64
|
||||||
if bucket == nil {
|
if err := database.Bolt.View(func(tx *bolt.Tx) error {
|
||||||
return errors.New("bucket 'pastes' could not be found")
|
bucket := tx.Bucket([]byte("pastes"))
|
||||||
}
|
if bucket == nil {
|
||||||
metric = float64(bucket.Stats().KeyN)
|
return errors.New("bucket 'pastes' could not be found")
|
||||||
return nil
|
}
|
||||||
}); err != nil {
|
metric = float64(bucket.Stats().KeyN)
|
||||||
log.Printf("error: %v", errors.Wrap(err, "fetching pastes_total metric"))
|
return nil
|
||||||
return 0
|
}); err != nil {
|
||||||
}
|
log.Printf("error: %v", errors.Wrap(err, "fetching pastes_total metric"))
|
||||||
return metric
|
return 0
|
||||||
})
|
}
|
||||||
)
|
return metric
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartMetricsServer starts sering Prometheus metrics exports on addr
|
||||||
|
func StartMetricsServer(addr string, database *db.Database, fs *db.FileStore) {
|
||||||
|
prometheus.MustRegister(metricRequestsTotalCounter)
|
||||||
|
|
||||||
|
prometheus.MustRegister(prometheus.NewGaugeFunc(prometheus.GaugeOpts{
|
||||||
|
Namespace: metricNamespace,
|
||||||
|
Subsystem: "pastes",
|
||||||
|
Name: "urls_total",
|
||||||
|
Help: "The current amount of pastes in the database.",
|
||||||
|
}, func() float64 { return metricURLsTotal(database) }))
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
router.Handle("/metrics", promhttp.Handler()).Methods("GET")
|
||||||
|
|||||||
103
router.go
103
router.go
@@ -4,19 +4,31 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.hashru.nl/dsprenkels/rushlink/internal/db"
|
"gitea.hashru.nl/dsprenkels/rushlink/internal/db"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const staticFilenameExpr = "[A-Za-z0-9-_.]+"
|
||||||
|
const urlKeyExpr = "{key:[A-Za-z0-9-_]{4,}}"
|
||||||
|
const urlKeyWithExtExpr = urlKeyExpr + "{ext:\\.[A-Za-z0-9-_]+}"
|
||||||
|
|
||||||
type rushlink struct {
|
type rushlink struct {
|
||||||
db *db.Database
|
db *db.Database
|
||||||
fs *db.FileStore
|
fs *db.FileStore
|
||||||
|
rootURL *url.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
func recoveryMiddleware(next http.Handler) http.Handler {
|
func (rl *rushlink) RootURL() *url.URL {
|
||||||
|
return rl.rootURL
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *rushlink) recoveryMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -30,33 +42,84 @@ func recoveryMiddleware(next http.Handler) http.Handler {
|
|||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
log.Printf("error: %v\n", err)
|
log.Printf("error: %v\n", err)
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
renderInternalServerError(w, r, err)
|
rl.renderInternalServerError(w, r, err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartMainServer(addr string, db *db.Database, fs *db.FileStore) {
|
func (rl *rushlink) metricsMiddleware(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
srw := statusResponseWriter{Inner: w}
|
||||||
|
next.ServeHTTP(&srw, r)
|
||||||
|
status := strconv.Itoa(srw.StatusCode)
|
||||||
|
metricRequestsTotalCounter.WithLabelValues(status, r.Method).Inc()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusResponseWriter struct {
|
||||||
|
Inner http.ResponseWriter
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusResponseWriter) Header() http.Header {
|
||||||
|
return w.Inner.Header()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusResponseWriter) Write(buf []byte) (int, error) {
|
||||||
|
if w.StatusCode == 0 {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return w.Inner.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.StatusCode = statusCode
|
||||||
|
w.Inner.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMainRouter creates the main Gorilla router for the application.
|
||||||
|
func CreateMainRouter(rl *rushlink) *mux.Router {
|
||||||
|
router := mux.NewRouter()
|
||||||
|
router.Use(rl.recoveryMiddleware)
|
||||||
|
router.Use(rl.metricsMiddleware)
|
||||||
|
router.HandleFunc("/{path:img/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/{path:css/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/{path:js/"+staticFilenameExpr+"}", rl.staticGetHandler).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/", rl.indexGetHandler).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/", rl.newPasteHandler).Methods("POST")
|
||||||
|
router.HandleFunc("/"+urlKeyExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/"+urlKeyWithExtExpr, rl.viewPasteHandler).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/"+urlKeyExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/"+urlKeyWithExtExpr+"/nr", rl.viewPasteHandlerNoRedirect).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/"+urlKeyExpr+"/meta", rl.viewPasteHandlerMeta).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/"+urlKeyWithExtExpr+"/meta", rl.viewPasteHandlerMeta).Methods("GET", "HEAD")
|
||||||
|
router.HandleFunc("/"+urlKeyExpr, rl.deletePasteHandler).Methods("DELETE")
|
||||||
|
router.HandleFunc("/"+urlKeyWithExtExpr, rl.deletePasteHandler).Methods("DELETE")
|
||||||
|
router.HandleFunc("/"+urlKeyExpr+"/delete", rl.deletePasteHandler).Methods("POST")
|
||||||
|
router.HandleFunc("/"+urlKeyWithExtExpr+"/delete", rl.deletePasteHandler).Methods("POST")
|
||||||
|
return router
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartMainServer starts the main http server listening on addr.
|
||||||
|
func StartMainServer(addr string, db *db.Database, fs *db.FileStore, rawRootURL string) {
|
||||||
|
var rootURL *url.URL
|
||||||
|
if rawRootURL != "" {
|
||||||
|
var err error
|
||||||
|
rootURL, err = url.Parse(rawRootURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalln(errors.Wrap(err, "could not parse rootURL flag"))
|
||||||
|
}
|
||||||
|
}
|
||||||
rl := rushlink{
|
rl := rushlink{
|
||||||
db: db,
|
db: db,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
|
rootURL: rootURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize Gorilla router
|
|
||||||
router := mux.NewRouter()
|
|
||||||
router.Use(recoveryMiddleware)
|
|
||||||
router.HandleFunc("/", rl.indexGetHandler).Methods("GET")
|
|
||||||
router.HandleFunc("/", rl.newPasteHandler).Methods("POST")
|
|
||||||
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", rl.viewPasteHandler).Methods("GET")
|
|
||||||
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/nr", rl.viewPasteHandlerNoRedirect).Methods("GET")
|
|
||||||
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/meta", rl.viewPasteHandlerMeta).Methods("GET")
|
|
||||||
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", rl.deletePasteHandler).Methods("DELETE")
|
|
||||||
router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/delete", rl.deletePasteHandler).Methods("POST")
|
|
||||||
router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", rl.uploadFileGetHandler).Methods("GET")
|
|
||||||
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Handler: router,
|
Handler: CreateMainRouter(&rl),
|
||||||
Addr: addr,
|
Addr: addr,
|
||||||
WriteTimeout: 15 * time.Second,
|
WriteTimeout: 15 * time.Second,
|
||||||
ReadTimeout: 15 * time.Second,
|
ReadTimeout: 15 * time.Second,
|
||||||
|
|||||||
78
views.go
78
views.go
@@ -16,10 +16,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
text "text/template"
|
text "text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultScheme = "http"
|
||||||
|
|
||||||
// Plain text templates
|
// Plain text templates
|
||||||
var textBaseTemplate *text.Template = text.Must(text.New("").Parse(string(MustAsset("templates/txt/base.txt.tmpl"))))
|
var textBaseTemplate *text.Template = text.Must(text.New("").Parse(string(MustAsset("templates/txt/base.txt.tmpl"))))
|
||||||
var htmlBaseTemplate *html.Template = html.Must(html.New("").Parse(string(MustAsset("templates/html/base.html.tmpl"))))
|
var htmlBaseTemplate *html.Template = html.Must(html.New("").Parse(string(MustAsset("templates/html/base.html.tmpl"))))
|
||||||
@@ -76,7 +79,27 @@ func parseFail(tmplName string, err error) {
|
|||||||
panic(errors.Wrapf(err, "parsing of %v failed", tmplName))
|
panic(errors.Wrapf(err, "parsing of %v failed", tmplName))
|
||||||
}
|
}
|
||||||
|
|
||||||
func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[string]interface{}) {
|
func mapExtend(m map[string]interface{}, key string, value interface{}) {
|
||||||
|
if m[key] != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *rushlink) renderStatic(w http.ResponseWriter, r *http.Request, path string) {
|
||||||
|
var modTime time.Time
|
||||||
|
if info, err := AssetInfo(path); err != nil {
|
||||||
|
modTime = info.ModTime()
|
||||||
|
}
|
||||||
|
contents, err := Asset(path)
|
||||||
|
if err != nil {
|
||||||
|
rl.renderError(w, r, http.StatusNotFound, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
http.ServeContent(w, r, path, modTime, bytes.NewReader(contents))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rl *rushlink) render(w http.ResponseWriter, r *http.Request, status int, tmplName string, data map[string]interface{}) {
|
||||||
contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"})
|
contentType, err := resolveResponseContentType(r, []string{"text/plain", "text/html"})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
w.WriteHeader(http.StatusNotAcceptable)
|
w.WriteHeader(http.StatusNotAcceptable)
|
||||||
@@ -84,7 +107,8 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add the request to the template data
|
// Add the request to the template data
|
||||||
data["Request"] = r
|
mapExtend(data, "RootURL", rl.resolveRootURL(r))
|
||||||
|
mapExtend(data, "Request", r)
|
||||||
|
|
||||||
switch contentType {
|
switch contentType {
|
||||||
case "text/plain":
|
case "text/plain":
|
||||||
@@ -115,7 +139,13 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
|
|||||||
}
|
}
|
||||||
return buf.String()
|
return buf.String()
|
||||||
}
|
}
|
||||||
err = tmpl.Execute(w, data)
|
if status != 0 {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Method != "HEAD" {
|
||||||
|
err = tmpl.Execute(w, data)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// Fall back to plain text without template
|
// Fall back to plain text without template
|
||||||
w.WriteHeader(http.StatusNotAcceptable)
|
w.WriteHeader(http.StatusNotAcceptable)
|
||||||
@@ -127,14 +157,46 @@ func render(w http.ResponseWriter, r *http.Request, tmplName string, data map[st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
|
func (rl *rushlink) renderError(w http.ResponseWriter, r *http.Request, status int, msg string) {
|
||||||
w.WriteHeader(status)
|
rl.render(w, r, status, "error", map[string]interface{}{"Message": msg})
|
||||||
render(w, r, "error", map[string]interface{}{"Message": msg})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
|
func (rl *rushlink) renderInternalServerError(w http.ResponseWriter, r *http.Request, err interface{}) {
|
||||||
msg := fmt.Sprintf("internal server error: %v", err)
|
msg := fmt.Sprintf("internal server error: %v", err)
|
||||||
renderError(w, r, http.StatusInternalServerError, msg)
|
rl.renderError(w, r, http.StatusInternalServerError, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveRootURL constructs the `scheme://host` part of rushlinks public API.
|
||||||
|
//
|
||||||
|
// If the `--root_url` flag is set, it will return that URL.
|
||||||
|
// Otherwise, this function will return 'https://{Host}', where `{Host}` is
|
||||||
|
// the value provided by the client in the HTTP `Host` header. This value may
|
||||||
|
// be invalid, but it is impossible to handle this error (because we *cannot*
|
||||||
|
// know the real host).
|
||||||
|
func (rl *rushlink) resolveRootURL(r *http.Request) string {
|
||||||
|
rlHost := rl.RootURL()
|
||||||
|
if rlHost != nil {
|
||||||
|
// Root URL overridden by command line arguments
|
||||||
|
return rlHost.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guess scheme
|
||||||
|
scheme := defaultScheme
|
||||||
|
forwardedScheme := r.Header.Get("X-Forwarded-Proto")
|
||||||
|
switch forwardedScheme {
|
||||||
|
case "http":
|
||||||
|
scheme = "http"
|
||||||
|
break
|
||||||
|
case "https":
|
||||||
|
scheme = "https"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Guess host
|
||||||
|
host := r.Host
|
||||||
|
if forwardedHost := r.Header.Get("X-Forwarded-Host"); forwardedHost != "" {
|
||||||
|
host = forwardedHost
|
||||||
|
}
|
||||||
|
return scheme + "://" + host
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to resolve the preferred content-type for the response to this request.
|
// Try to resolve the preferred content-type for the response to this request.
|
||||||
|
|||||||
33
views_test.go
Normal file
33
views_test.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package rushlink
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveResponseContentTypeSuccess(t *testing.T, expected string, types []string, acceptVal string) {
|
||||||
|
r, err := http.NewRequest("HEAD", "", nil)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
r.Header.Set("Accept", acceptVal)
|
||||||
|
got, err := resolveResponseContentType(r, types)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("error: '%v'\n", err)
|
||||||
|
}
|
||||||
|
if got != expected {
|
||||||
|
t.Errorf("error: '%v' should be '%v'\n", got, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveResponseContentType(t *testing.T) {
|
||||||
|
resolveResponseContentTypeSuccess(t, "", []string{}, "text/html")
|
||||||
|
resolveResponseContentTypeSuccess(t, "text/html", []string{"text/html"}, "")
|
||||||
|
resolveResponseContentTypeSuccess(t, "text/html", []string{"text/txt", "text/html"}, "text/txt;q=0.5,text/html")
|
||||||
|
resolveResponseContentTypeSuccess(t, "text/html", []string{"text/txt", "text/html"}, "text/txt;q=0.5,text/html;q=0.9")
|
||||||
|
resolveResponseContentTypeSuccess(t, "", []string{"text"}, "text/html")
|
||||||
|
resolveResponseContentTypeSuccess(t, "", []string{"text/*"}, "image/*")
|
||||||
|
|
||||||
|
// Issue #17
|
||||||
|
resolveResponseContentTypeSuccess(t, "*/*", []string{"*/*"}, "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user