Add option to force random filenames (fixes #86) (#159)

This commit is contained in:
mutantmonkey 2019-01-26 10:04:32 +00:00 committed by Andrei Marcu
parent a79bc1898a
commit 8f3108148b
9 changed files with 209 additions and 103 deletions

View file

@ -52,6 +52,7 @@ allowhotlink = true
- ```-xframeoptions "..." ``` -- X-Frame-Options header (default is "SAMEORIGIN") - ```-xframeoptions "..." ``` -- X-Frame-Options header (default is "SAMEORIGIN")
- ```-remoteuploads``` -- (optionally) enable remote uploads (/upload?url=https://...) - ```-remoteuploads``` -- (optionally) enable remote uploads (/upload?url=https://...)
- ```-nologs``` -- (optionally) disable request logs in stdout - ```-nologs``` -- (optionally) disable request logs in stdout
- ```-force-random-filename``` -- (optionally) force the use of random filenames
#### Storage backends #### Storage backends
The following storage backends are available: The following storage backends are available:

View file

@ -21,8 +21,9 @@ const (
func indexHandler(c web.C, w http.ResponseWriter, r *http.Request) { func indexHandler(c web.C, w http.ResponseWriter, r *http.Request) {
err := renderTemplate(Templates["index.html"], pongo2.Context{ err := renderTemplate(Templates["index.html"], pongo2.Context{
"maxsize": Config.maxSize, "maxsize": Config.maxSize,
"expirylist": listExpirationTimes(), "expirylist": listExpirationTimes(),
"forcerandom": Config.forceRandomFilename,
}, r, w) }, r, w)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
@ -31,7 +32,8 @@ func indexHandler(c web.C, w http.ResponseWriter, r *http.Request) {
func pasteHandler(c web.C, w http.ResponseWriter, r *http.Request) { func pasteHandler(c web.C, w http.ResponseWriter, r *http.Request) {
err := renderTemplate(Templates["paste.html"], pongo2.Context{ err := renderTemplate(Templates["paste.html"], pongo2.Context{
"expirylist": listExpirationTimes(), "expirylist": listExpirationTimes(),
"forcerandom": Config.forceRandomFilename,
}, r, w) }, r, w)
if err != nil { if err != nil {
oopsHandler(c, w, r, RespHTML, "") oopsHandler(c, w, r, RespHTML, "")
@ -40,7 +42,8 @@ func pasteHandler(c web.C, w http.ResponseWriter, r *http.Request) {
func apiDocHandler(c web.C, w http.ResponseWriter, r *http.Request) { func apiDocHandler(c web.C, w http.ResponseWriter, r *http.Request) {
err := renderTemplate(Templates["API.html"], pongo2.Context{ err := renderTemplate(Templates["API.html"], pongo2.Context{
"siteurl": getSiteURL(r), "siteurl": getSiteURL(r),
"forcerandom": Config.forceRandomFilename,
}, r, w) }, r, w)
if err != nil { if err != nil {
oopsHandler(c, w, r, RespHTML, "") oopsHandler(c, w, r, RespHTML, "")

View file

@ -65,6 +65,7 @@ var Config struct {
s3Region string s3Region string
s3Bucket string s3Bucket string
s3ForcePathStyle bool s3ForcePathStyle bool
forceRandomFilename bool
} }
var Templates = make(map[string]*pongo2.Template) var Templates = make(map[string]*pongo2.Template)
@ -268,6 +269,8 @@ func main() {
"S3 bucket to use for files and metadata") "S3 bucket to use for files and metadata")
flag.BoolVar(&Config.s3ForcePathStyle, "s3-force-path-style", false, flag.BoolVar(&Config.s3ForcePathStyle, "s3-force-path-style", false,
"Force path-style addressing for S3 (e.g. https://s3.amazonaws.com/linx/example.txt)") "Force path-style addressing for S3 (e.g. https://s3.amazonaws.com/linx/example.txt)")
flag.BoolVar(&Config.forceRandomFilename, "force-random-filename", false,
"Force all uploads to use a random filename")
iniflags.Parse() iniflags.Parse()

View file

@ -763,6 +763,32 @@ func TestPutRandomizedUpload(t *testing.T) {
} }
} }
func TestPutForceRandomUpload(t *testing.T) {
mux := setup()
w := httptest.NewRecorder()
oldFRF := Config.forceRandomFilename
Config.forceRandomFilename = true
filename := "randomizeme.file"
req, err := http.NewRequest("PUT", "/upload/"+filename, strings.NewReader("File content"))
if err != nil {
t.Fatal(err)
}
// while this should also work without this header, let's try to force
// the randomized filename off to be sure
req.Header.Set("Linx-Randomize", "no")
mux.ServeHTTP(w, req)
if w.Body.String() == Config.siteURL+filename {
t.Fatal("Filename was not random")
}
Config.forceRandomFilename = oldFRF
}
func TestPutNoExtensionUpload(t *testing.T) { func TestPutNoExtensionUpload(t *testing.T) {
mux := setup() mux := setup()
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -1013,6 +1039,55 @@ func TestPutAndOverwrite(t *testing.T) {
} }
} }
func TestPutAndOverwriteForceRandom(t *testing.T) {
var myjson RespOkJSON
mux := setup()
w := httptest.NewRecorder()
oldFRF := Config.forceRandomFilename
Config.forceRandomFilename = true
req, err := http.NewRequest("PUT", "/upload", strings.NewReader("File content"))
if err != nil {
t.Fatal(err)
}
req.Header.Set("Accept", "application/json")
mux.ServeHTTP(w, req)
err = json.Unmarshal([]byte(w.Body.String()), &myjson)
if err != nil {
t.Fatal(err)
}
// Overwrite it
w = httptest.NewRecorder()
req, err = http.NewRequest("PUT", "/upload/"+myjson.Filename, strings.NewReader("New file content"))
req.Header.Set("Linx-Delete-Key", myjson.Delete_Key)
mux.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatal("Status code was not 200, but " + strconv.Itoa(w.Code))
}
// Make sure it's the new file
w = httptest.NewRecorder()
req, err = http.NewRequest("GET", "/"+Config.selifPath+myjson.Filename, nil)
mux.ServeHTTP(w, req)
if w.Code == 404 {
t.Fatal("Status code was 404")
}
if w.Body.String() != "New file content" {
t.Fatal("File did not contain 'New file content")
}
Config.forceRandomFilename = oldFRF
}
func TestPutAndSpecificDelete(t *testing.T) { func TestPutAndSpecificDelete(t *testing.T) {
var myjson RespOkJSON var myjson RespOkJSON

View file

@ -1,51 +1,54 @@
// @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later
Dropzone.options.dropzone = { Dropzone.options.dropzone = {
init: function() { init: function() {
var dzone = document.getElementById("dzone"); var dzone = document.getElementById("dzone");
dzone.style.display = "block"; dzone.style.display = "block";
}, },
addedfile: function(file) { addedfile: function(file) {
var upload = document.createElement("div"); var upload = document.createElement("div");
upload.className = "upload"; upload.className = "upload";
var fileLabel = document.createElement("span"); var fileLabel = document.createElement("span");
fileLabel.innerHTML = file.name; fileLabel.innerHTML = file.name;
file.fileLabel = fileLabel; file.fileLabel = fileLabel;
upload.appendChild(fileLabel); upload.appendChild(fileLabel);
var fileActions = document.createElement("div"); var fileActions = document.createElement("div");
fileActions.className = "right"; fileActions.className = "right";
file.fileActions = fileActions; file.fileActions = fileActions;
upload.appendChild(fileActions); upload.appendChild(fileActions);
var cancelAction = document.createElement("span"); var cancelAction = document.createElement("span");
cancelAction.className = "cancel"; cancelAction.className = "cancel";
cancelAction.innerHTML = "Cancel"; cancelAction.innerHTML = "Cancel";
cancelAction.addEventListener('click', function(ev) { cancelAction.addEventListener('click', function(ev) {
this.removeFile(file); this.removeFile(file);
}.bind(this)); }.bind(this));
file.cancelActionElement = cancelAction; file.cancelActionElement = cancelAction;
fileActions.appendChild(cancelAction); fileActions.appendChild(cancelAction);
var progress = document.createElement("span"); var progress = document.createElement("span");
file.progressElement = progress; file.progressElement = progress;
fileActions.appendChild(progress); fileActions.appendChild(progress);
file.uploadElement = upload; file.uploadElement = upload;
document.getElementById("uploads").appendChild(upload); document.getElementById("uploads").appendChild(upload);
}, },
uploadprogress: function(file, p, bytesSent) { uploadprogress: function(file, p, bytesSent) {
p = parseInt(p); p = parseInt(p);
file.progressElement.innerHTML = p + "%"; file.progressElement.innerHTML = p + "%";
file.uploadElement.setAttribute("style", 'background-image: -webkit-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -moz-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -ms-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -o-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%)'); file.uploadElement.setAttribute("style", 'background-image: -webkit-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -moz-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -ms-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: -o-linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%); background-image: linear-gradient(left, #F2F4F7 ' + p + '%, #E2E2E2 ' + p + '%)');
}, },
sending: function(file, xhr, formData) { sending: function(file, xhr, formData) {
formData.append("randomize", document.getElementById("randomize").checked); var randomize = document.getElementById("randomize");
formData.append("expires", document.getElementById("expires").value); if(randomize != null) {
}, formData.append("randomize", randomize.checked);
success: function(file, resp) { }
formData.append("expires", document.getElementById("expires").value);
},
success: function(file, resp) {
file.fileActions.removeChild(file.progressElement); file.fileActions.removeChild(file.progressElement);
var fileLabelLink = document.createElement("a"); var fileLabelLink = document.createElement("a");
@ -59,61 +62,61 @@ Dropzone.options.dropzone = {
var deleteAction = document.createElement("span"); var deleteAction = document.createElement("span");
deleteAction.innerHTML = "Delete"; deleteAction.innerHTML = "Delete";
deleteAction.className = "cancel"; deleteAction.className = "cancel";
deleteAction.addEventListener('click', function(ev) { deleteAction.addEventListener('click', function(ev) {
xhr = new XMLHttpRequest(); xhr = new XMLHttpRequest();
xhr.open("DELETE", resp.url, true); xhr.open("DELETE", resp.url, true);
xhr.setRequestHeader("Linx-Delete-Key", resp.delete_key); xhr.setRequestHeader("Linx-Delete-Key", resp.delete_key);
xhr.onreadystatechange = function(file) { xhr.onreadystatechange = function(file) {
if (xhr.readyState == 4 && xhr.status === 200) { if (xhr.readyState == 4 && xhr.status === 200) {
var text = document.createTextNode("Deleted "); var text = document.createTextNode("Deleted ");
file.fileLabel.insertBefore(text, file.fileLabelLink); file.fileLabel.insertBefore(text, file.fileLabelLink);
file.fileLabel.className = "deleted"; file.fileLabel.className = "deleted";
file.fileActions.removeChild(file.cancelActionElement); file.fileActions.removeChild(file.cancelActionElement);
} }
}.bind(this, file); }.bind(this, file);
xhr.send(); xhr.send();
}); });
file.fileActions.removeChild(file.cancelActionElement); file.fileActions.removeChild(file.cancelActionElement);
file.cancelActionElement = deleteAction; file.cancelActionElement = deleteAction;
file.fileActions.appendChild(deleteAction); file.fileActions.appendChild(deleteAction);
}, },
error: function(file, resp, xhrO) { error: function(file, resp, xhrO) {
file.fileActions.removeChild(file.cancelActionElement); file.fileActions.removeChild(file.cancelActionElement);
file.fileActions.removeChild(file.progressElement); file.fileActions.removeChild(file.progressElement);
if (file.status === "canceled") { if (file.status === "canceled") {
file.fileLabel.innerHTML = file.name + ": Canceled "; file.fileLabel.innerHTML = file.name + ": Canceled ";
} }
else { else {
if (resp.error) { if (resp.error) {
file.fileLabel.innerHTML = file.name + ": " + resp.error; file.fileLabel.innerHTML = file.name + ": " + resp.error;
} }
else if (resp.includes("<html")) { else if (resp.includes("<html")) {
file.fileLabel.innerHTML = file.name + ": Server Error"; file.fileLabel.innerHTML = file.name + ": Server Error";
} }
else { else {
file.fileLabel.innerHTML = file.name + ": " + resp; file.fileLabel.innerHTML = file.name + ": " + resp;
} }
} }
file.fileLabel.className = "error"; file.fileLabel.className = "error";
}, },
maxFilesize: Math.round(parseInt(document.getElementById("dropzone").getAttribute("data-maxsize"), 10) / 1024 / 1024), maxFilesize: Math.round(parseInt(document.getElementById("dropzone").getAttribute("data-maxsize"), 10) / 1024 / 1024),
previewsContainer: "#uploads", previewsContainer: "#uploads",
parallelUploads: 5, parallelUploads: 5,
headers: {"Accept": "application/json"}, headers: {"Accept": "application/json"},
dictDefaultMessage: "Click or Drop file(s) or Paste image", dictDefaultMessage: "Click or Drop file(s) or Paste image",
dictFallbackMessage: "" dictFallbackMessage: ""
}; };
document.onpaste = function(event) { document.onpaste = function(event) {
var items = (event.clipboardData || event.originalEvent.clipboardData).items; var items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (index in items) { for (index in items) {
var item = items[index]; var item = items[index];
if (item.kind === "file") { if (item.kind === "file") {
Dropzone.forElement("#dropzone").addFile(item.getAsFile()); Dropzone.forElement("#dropzone").addFile(item.getAsFile());
} }
} }
}; };
// @end-license // @end-license

View file

@ -25,8 +25,10 @@
<p><strong>Optional headers with the request</strong></p> <p><strong>Optional headers with the request</strong></p>
{% if not forcerandom %}
<p>Randomize the filename<br/> <p>Randomize the filename<br/>
<code>Linx-Randomize: yes</code></p> <code>Linx-Randomize: yes</code></p>
{% endif %}
<p>Specify a custom deletion key<br/> <p>Specify a custom deletion key<br/>
<code>Linx-Delete-Key: mysecret</code></p> <code>Linx-Delete-Key: mysecret</code></p>
@ -56,30 +58,30 @@
{% if using_auth %} {% if using_auth %}
<pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -T myphoto.jpg {{ siteurl }}upload/
{{ siteurl }}myphoto.jpg</code></pre> {{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}7z4h4ut.jpg{% endif %}</code></pre>
{% else %} {% else %}
<pre><code>$ curl -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -T myphoto.jpg {{ siteurl }}upload/
{{ siteurl }}myphoto.jpg</code></pre> {{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}wtq7pan.jpg{% endif %}</code></pre>
{% endif %} {% endif %}
<p>Uploading myphoto.jpg with an expiry of 20 minutes</p> <p>Uploading myphoto.jpg with an expiry of 20 minutes</p>
{% if using_auth %} {% if using_auth %}
<pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Expiry: 1200&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Linx-Expiry: 1200&#34; -T myphoto.jpg {{ siteurl }}upload/
{{ siteurl }}myphoto.jpg</code></pre> {{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}jm295snf.jpg{% endif %}</code></pre>
{% else %} {% else %}
<pre><code>$ curl -H &#34;Linx-Expiry: 1200&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Expiry: 1200&#34; -T myphoto.jpg {{ siteurl }}upload/
{{ siteurl }}myphoto.jpg</code></pre> {{ siteurl }}{% if not forcerandom %}myphoto.jpg{% else %}1doym9u2.jpg{% endif %}</code></pre>
{% endif %} {% endif %}
<p>Uploading myphoto.jpg with a random filename and getting a json response:</p> <p>Uploading myphoto.jpg with a random filename and getting a json response:</p>
{% if using_auth %} {% if using_auth %}
<pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Accept: application/json&#34; -H &#34;Linx-Randomize: yes&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Linx-Api-Key: mysecretkey&#34; -H &#34;Accept: application/json&#34;{% if not forcerandom %} -H &#34;Linx-Randomize: yes&#34;{% endif %} -T myphoto.jpg {{ siteurl }}upload/
{&#34;delete_key&#34;:&#34;...&#34;,&#34;expiry&#34;:&#34;0&#34;,&#34;filename&#34;:&#34;f34h4iu.jpg&#34;,&#34;mimetype&#34;:&#34;image/jpeg&#34;, {&#34;delete_key&#34;:&#34;...&#34;,&#34;expiry&#34;:&#34;0&#34;,&#34;filename&#34;:&#34;f34h4iu.jpg&#34;,&#34;mimetype&#34;:&#34;image/jpeg&#34;,
&#34;sha256sum&#34;:&#34;...&#34;,&#34;size&#34;:&#34;...&#34;,&#34;url&#34;:&#34;{{ siteurl }}f34h4iu.jpg&#34;}</code></pre> &#34;sha256sum&#34;:&#34;...&#34;,&#34;size&#34;:&#34;...&#34;,&#34;url&#34;:&#34;{{ siteurl }}f34h4iu.jpg&#34;}</code></pre>
{% else %} {% else %}
<pre><code>$ curl -H &#34;Accept: application/json&#34; -H &#34;Linx-Randomize: yes&#34; -T myphoto.jpg {{ siteurl }}upload/ <pre><code>$ curl -H &#34;Accept: application/json&#34;{% if not forcerandom %} -H &#34;Linx-Randomize: yes&#34;{% endif %} -T myphoto.jpg {{ siteurl }}upload/
{&#34;delete_key&#34;:&#34;...&#34;,&#34;expiry&#34;:&#34;0&#34;,&#34;filename&#34;:&#34;f34h4iu.jpg&#34;,&#34;mimetype&#34;:&#34;image/jpeg&#34;, {&#34;delete_key&#34;:&#34;...&#34;,&#34;expiry&#34;:&#34;0&#34;,&#34;filename&#34;:&#34;f34h4iu.jpg&#34;,&#34;mimetype&#34;:&#34;image/jpeg&#34;,
&#34;sha256sum&#34;:&#34;...&#34;,&#34;size&#34;:&#34;...&#34;,&#34;url&#34;:&#34;{{ siteurl }}f34h4iu.jpg&#34;}</code></pre> &#34;sha256sum&#34;:&#34;...&#34;,&#34;size&#34;:&#34;...&#34;,&#34;url&#34;:&#34;{{ siteurl }}f34h4iu.jpg&#34;}</code></pre>
{% endif %} {% endif %}

View file

@ -17,7 +17,7 @@
</div> </div>
<div id="choices"> <div id="choices">
<label><input name="randomize" id="randomize" type="checkbox" checked /> Randomize filename</label> <label>{% if not forcerandom %}<input name="randomize" id="randomize" type="checkbox" checked /> Randomize filename{% endif %}</label>
<div id="expiry"> <div id="expiry">
<label>File expiry: <label>File expiry:
<select name="expires" id="expires"> <select name="expires" id="expires">

View file

@ -5,7 +5,7 @@
<div id="main" class="paste"> <div id="main" class="paste">
<div id="info"> <div id="info">
<div> <div>
<span class="hint--top hint--bounce" data-hint="Leave empty for random filename"><input class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename" /></span>.<span class="hint--top hint--bounce" data-hint="Enable syntax highlighting by adding the extension"><input id="extension" class="codebox" name='extension' type='text' value="" placeholder="txt" /></span> {% if not forcerandom %}<span class="hint--top hint--bounce" data-hint="Leave empty for random filename"><input class="codebox" name='filename' id="filename" type='text' value="" placeholder="filename" /></span>{% endif %}.<span class="hint--top hint--bounce" data-hint="Enable syntax highlighting by adding the extension"><input id="extension" class="codebox" name='extension' type='text' value="" placeholder="txt" /></span>
</div> </div>
<div> <div>
<input type="submit" value="Paste"> <input type="submit" value="Paste">

View file

@ -222,11 +222,14 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
return upload, FileTooLargeError return upload, FileTooLargeError
} }
// Determine the appropriate filename, then write to disk // Determine the appropriate filename
barename, extension := barePlusExt(upReq.filename) barename, extension := barePlusExt(upReq.filename)
randomize := false
// Randomize the "barename" (filename without extension) if needed
if upReq.randomBarename || len(barename) == 0 { if upReq.randomBarename || len(barename) == 0 {
barename = generateBarename() barename = generateBarename()
randomize = true
} }
var header []byte var header []byte
@ -259,16 +262,32 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
if merr == nil { if merr == nil {
if upReq.deleteKey == metad.DeleteKey { if upReq.deleteKey == metad.DeleteKey {
fileexists = false fileexists = false
} else if Config.forceRandomFilename == true {
// the file exists
// the delete key doesn't match
// force random filenames is enabled
randomize = true
} }
} }
} else if Config.forceRandomFilename == true {
// the file doesn't exist
// force random filenames is enabled
randomize = true
// set fileexists to true to generate a new barename
fileexists = true
} }
for fileexists { for fileexists {
counter, err := strconv.Atoi(string(barename[len(barename)-1])) if randomize {
if err != nil { barename = generateBarename()
barename = barename + "1"
} else { } else {
barename = barename[:len(barename)-1] + strconv.Itoa(counter+1) counter, err := strconv.Atoi(string(barename[len(barename)-1]))
if err != nil {
barename = barename + "1"
} else {
barename = barename[:len(barename)-1] + strconv.Itoa(counter+1)
}
} }
upload.Filename = strings.Join([]string{barename, extension}, ".") upload.Filename = strings.Join([]string{barename, extension}, ".")