diff --git a/Dockerfile b/Dockerfile index 8f02dcc..addad3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,26 @@ -FROM golang:alpine +FROM golang:alpine3.8 AS build + +COPY . /go/src/github.com/andreimarcu/linx-server +WORKDIR /go/src/github.com/andreimarcu/linx-server RUN set -ex \ && apk add --no-cache --virtual .build-deps git \ - && go get github.com/andreimarcu/linx-server \ + && go get -v . \ && apk del .build-deps +FROM alpine:3.8 + +COPY --from=build /go/bin/linx-server /usr/local/bin/linx-server + +ENV GOPATH /go +COPY static /go/src/github.com/andreimarcu/linx-server/static/ +COPY templates /go/src/github.com/andreimarcu/linx-server/templates/ + RUN mkdir -p /data/files && mkdir -p /data/meta && chown -R 65534:65534 /data VOLUME ["/data/files", "/data/meta"] EXPOSE 8080 USER nobody -ENTRYPOINT ["/go/bin/linx-server", "-bind=0.0.0.0:8080", "-filespath=/data/files/", "-metapath=/data/meta/"] +ENTRYPOINT ["/usr/local/bin/linx-server", "-bind=0.0.0.0:8080", "-filespath=/data/files/", "-metapath=/data/meta/"] CMD ["-sitename=linx", "-allowhotlink"] diff --git a/README.md b/README.md index df13b18..8d4ef11 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ allowhotlink = true - ```-bind 127.0.0.1:8080``` -- what to bind to (default is 127.0.0.1:8080) - ```-sitename myLinx``` -- the site name displayed on top (default is inferred from Host header) - ```-siteurl "http://mylinx.example.org/"``` -- the site url (default is inferred from execution context) +- ```-selifpath "selif"``` -- path relative to site base url (the "selif" in https://mylinx.example.org/selif/image.jpg) where files are accessed directly (default: selif) - ```-filespath files/``` -- Path to store uploads (default is files/) - ```-metapath meta/``` -- Path to store information about uploads (default is meta/) - ```-maxsize 4294967296``` -- maximum upload file size in bytes (default 4GB) diff --git a/csp_test.go b/csp_test.go index 3d1d499..e3dbbdd 100644 --- a/csp_test.go +++ b/csp_test.go @@ -23,6 +23,7 @@ func TestContentSecurityPolicy(t *testing.T) { Config.maxSize = 1024 * 1024 * 1024 Config.noLogs = true Config.siteName = "linx" + Config.selifPath = "selif" Config.contentSecurityPolicy = testCSPHeaders["Content-Security-Policy"] Config.referrerPolicy = testCSPHeaders["Referrer-Policy"] Config.xFrameOptions = testCSPHeaders["X-Frame-Options"] diff --git a/display.go b/display.go index 00b77d5..fbc917c 100644 --- a/display.go +++ b/display.go @@ -24,7 +24,7 @@ const maxDisplayFileSizeBytes = 1024 * 512 var cliUserAgentRe = regexp.MustCompile("(?i)(lib)?curl|wget") func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) { - if !Config.noDirectAgents && cliUserAgentRe.MatchString(r.Header.Get("User-Agent")) { + if !Config.noDirectAgents && cliUserAgentRe.MatchString(r.Header.Get("User-Agent")) && !strings.EqualFold("application/json", r.Header.Get("Accept")) { fileServeHandler(c, w, r) return } @@ -54,11 +54,12 @@ func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) { if strings.EqualFold("application/json", r.Header.Get("Accept")) { js, _ := json.Marshal(map[string]string{ - "filename": fileName, - "expiry": strconv.FormatInt(metadata.Expiry.Unix(), 10), - "size": strconv.FormatInt(metadata.Size, 10), - "mimetype": metadata.Mimetype, - "sha256sum": metadata.Sha256sum, + "filename": fileName, + "direct_url": getSiteURL(r) + Config.selifPath + fileName, + "expiry": strconv.FormatInt(metadata.Expiry.Unix(), 10), + "size": strconv.FormatInt(metadata.Size, 10), + "mimetype": metadata.Mimetype, + "sha256sum": metadata.Sha256sum, }) w.Write(js) return @@ -133,13 +134,14 @@ func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) { } err = renderTemplate(tpl, pongo2.Context{ - "mime": metadata.Mimetype, - "filename": fileName, - "size": sizeHuman, - "expiry": expiryHuman, - "extra": extra, - "lines": lines, - "files": metadata.ArchiveFiles, + "mime": metadata.Mimetype, + "filename": fileName, + "size": sizeHuman, + "expiry": expiryHuman, + "expirylist": listExpirationTimes(), + "extra": extra, + "lines": lines, + "files": metadata.ArchiveFiles, }, r, w) if err != nil { diff --git a/server.go b/server.go index 4af3ea6..851a7cf 100644 --- a/server.go +++ b/server.go @@ -42,6 +42,7 @@ var Config struct { siteName string siteURL string sitePath string + selifPath string certFile string keyFile string contentSecurityPolicy string @@ -131,6 +132,11 @@ func setup() *web.Mux { Config.sitePath = "/" } + Config.selifPath = strings.TrimLeft(Config.selifPath, "/") + if lastChar := Config.selifPath[len(Config.selifPath)-1:]; lastChar != "/" { + Config.selifPath = Config.selifPath + "/" + } + if Config.s3Bucket != "" { storageBackend = s3.NewS3Backend(Config.s3Bucket, Config.s3Region, Config.s3Endpoint) } else { @@ -154,8 +160,8 @@ func setup() *web.Mux { // Routing setup nameRe := regexp.MustCompile("^" + Config.sitePath + `(?P[a-z0-9-\.]+)$`) - selifRe := regexp.MustCompile("^" + Config.sitePath + `selif/(?P[a-z0-9-\.]+)$`) - selifIndexRe := regexp.MustCompile("^" + Config.sitePath + `selif/$`) + selifRe := regexp.MustCompile("^" + Config.sitePath + Config.selifPath + `(?P[a-z0-9-\.]+)$`) + selifIndexRe := regexp.MustCompile("^" + Config.sitePath + Config.selifPath + `$`) torrentRe := regexp.MustCompile("^" + Config.sitePath + `(?P[a-z0-9-\.]+)/torrent$`) if Config.authFile == "" { @@ -215,6 +221,8 @@ func main() { "name of the site") flag.StringVar(&Config.siteURL, "siteurl", "", "site base url (including trailing slash)") + flag.StringVar(&Config.selifPath, "selifpath", "selif", + "path relative to site base url where files are accessed directly") flag.Int64Var(&Config.maxSize, "maxsize", 4*1024*1024*1024, "maximum upload file size in bytes (default 4GB)") flag.Uint64Var(&Config.maxExpiry, "maxexpiry", 0, diff --git a/server_test.go b/server_test.go index 64c230b..a1ec853 100644 --- a/server_test.go +++ b/server_test.go @@ -173,7 +173,7 @@ func TestFileNotFound(t *testing.T) { filename := generateBarename() - req, err := http.NewRequest("GET", "/selif/"+filename, nil) + req, err := http.NewRequest("GET", "/"+Config.selifPath+filename, nil) if err != nil { t.Fatal(err) } @@ -1001,7 +1001,7 @@ func TestPutAndOverwrite(t *testing.T) { // Make sure it's the new file w = httptest.NewRecorder() - req, err = http.NewRequest("GET", "/selif/"+myjson.Filename, nil) + req, err = http.NewRequest("GET", "/"+Config.selifPath+myjson.Filename, nil) mux.ServeHTTP(w, req) if w.Code == 404 { diff --git a/static/css/dropzone.css b/static/css/dropzone.css index 18472a6..e19794f 100644 --- a/static/css/dropzone.css +++ b/static/css/dropzone.css @@ -31,17 +31,25 @@ border: 2px solid #FAFBFC; } -#dropzone { width: 400px; +#dropzone { + width: 400px; margin-left: auto; margin-right: auto; } +@media(max-width: 450px) { + #dropzone { + width: auto; + } +} + #uploads { margin-top: 20px; } div.dz-default { border: 2px dashed #C9C9C9; + border-radius: 5px; color: #C9C9C9; font: 14px "helvetica neue",helvetica,arial,sans-serif; background-color: #FAFBFC; diff --git a/static/css/github-markdown.css b/static/css/github-markdown.css index 8072b54..6823d9c 100644 --- a/static/css/github-markdown.css +++ b/static/css/github-markdown.css @@ -8,7 +8,8 @@ font-size: 12px; line-height: 1.6; word-wrap: break-word; - width: 680px; + width: 80vw; + max-width: 680px; padding: 10px; } diff --git a/static/css/linx.css b/static/css/linx.css index 193cbec..82e256a 100644 --- a/static/css/linx.css +++ b/static/css/linx.css @@ -1,56 +1,56 @@ body { - background-color: #E8ECF0; - color: #556A7F; + background-color: #E8ECF0; + color: #556A7F; - font-family: Arial, Helvetica, sans-serif; - font-size: 14px; + font-family: Arial, Helvetica, sans-serif; + font-size: 14px; } #container_container { - display: table; - table-layout: fixed; - margin-left: auto; - margin-right: auto; + display: table; + table-layout: fixed; + margin-left: auto; + margin-right: auto; } #container { - display: table-cell; - min-width: 200px; + display: table-cell; + min-width: 200px; } #header a { - text-decoration: none; - color: #556A7F; + text-decoration: none; + color: #556A7F; } #navigation { - margin-top: 4px; + margin-top: 4px; } #navigation a { - text-decoration: none; - border-bottom: 1px dotted #556A7F; - color: #556A7F; + text-decoration: none; + border-bottom: 1px dotted #556A7F; + color: #556A7F; } #navigation a:hover { - background-color: #C7D1EB; + background-color: #C7D1EB; } #main { - background-color: white; + background-color: white; - padding: 6px 5px 8px 5px; + padding: 6px 5px 8px 5px; - -moz-box-shadow: 1px 1px 1px 1px #ccc; - -webkit-box-shadow: 1px 1px 1px 1px #ccc; - box-shadow: 1px 1px 1px 1px #ccc; + -moz-box-shadow: 1px 1px 1px 1px #ccc; + -webkit-box-shadow: 1px 1px 1px 1px #ccc; + box-shadow: 1px 1px 1px 1px #ccc; - text-align: center; + text-align: center; } #main a { - color: #556A7F; + color: #556A7F; } #normal-content { @@ -62,28 +62,19 @@ body { margin-bottom: 0; } -.ninfo { - margin-bottom: 5px; -} - .dinfo { -moz-box-shadow: 1px 1px 1px 1px #ccc; -webkit-box-shadow: 1px 1px 1px 1px #ccc; box-shadow: 1px 1px 1px 1px #ccc; margin-bottom: 15px; - } #info { - text-align: left; - + display: flex; + flex-wrap: wrap; + justify-content: space-between; background-color: white; - padding: 5px 5px 5px 5px; -} - -#info #filename, -#editform #filename { - width: 232px; + padding: 5px 5px 5px 5px; } #info #extension, @@ -91,13 +82,8 @@ body { width: 40px; } -#info .float-left { - margin-top: 2px; - margin-right: 20px; -} - -#info .right { - font-size: 13px; +#info .text-right { + font-size: 13px; } #info a { @@ -110,88 +96,97 @@ body { background-color: #E8ECF0; } -#info input[type=text] { - border: 0; - color: #556A7F; +#info input[type=checkbox] { + margin: 0; + vertical-align: bottom; } #footer { - color: gray; - text-align: right; - margin-top: 30px; - margin-bottom: 10px; - font-size: 11px; + color: gray; + text-align: right; + margin-top: 30px; + margin-bottom: 10px; + font-size: 11px; } #footer a { - color: gray; - text-decoration: none; + color: gray; + text-decoration: none; } - .normal { - text-align: left; - font-size: 13px; + text-align: left; + font-size: 13px; } .normal a { - text-decoration: none; - border-bottom: 1px dotted gray; + text-decoration: none; + border-bottom: 1px dotted gray; } .normal a:hover { - color: black; - background-color: #E8ECF0; + color: black; + background-color: #E8ECF0; } .normal ul { - padding-left: 15px; + padding-left: 15px; } .normal li { - margin-bottom: 3px; - list-style: none; + margin-bottom: 3px; + list-style: none; } .normal li a { - font-weight: bold; + font-weight: bold; } .fixed { - width: 800px; + width: 80vw; + max-width: 800px; +} + +.paste { + width: 70vw; + max-width: 700px; } .needs-border { - border-top: 1px solid rgb(214, 214, 214); + border-top: 1px solid rgb(214, 214, 214); } .left { - text-align: left; + text-align: left; } .float-left { - float: left; + float: left; +} + +.pad-left { + padding-left: 10px; } .pad-right { - padding-right: 10px; + padding-right: 10px; } .text-right { - text-align: right; + text-align: right; } .center { - text-align: center; + text-align: center; } .float-right, .right { - float: right; + float: right; } .clear { - clear: both; + clear: both; } #upload_header { @@ -245,19 +240,28 @@ body { } #choices { - float: left; - width: 100%; - text-align: left; - vertical-align: bottom; - margin-top: 5px; + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: space-between; + width: 100%; + margin-top: 5px; font-size:13px; } +#choices label:first-child { + margin-right: 15px; +} + #expiry { - float: right; padding-top: 1px; } +#randomize { + vertical-align: bottom; + margin: 0; +} + .oopscontent { width: 400px; } @@ -267,13 +271,38 @@ body { border: 0; } +.error-404 img { + max-width: 90vw; +} + +.padme { + padding-left: 5px; + padding-right: 5px; +} + .editor { - width: 705px; - height: 450px; - border-color: #cccccc; - font-family: monospace; - resize: none; - overflow: auto; + width: 100%; + height: 450px; + border: 1px solid #eaeaea; + font-family: monospace; + resize: none; + overflow: auto; + border-radius: 2px; + padding: 2px; + box-sizing: border-box; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + } + + + +#info input[type=text] { + border: 1px solid #eaeaea; + color: #556A7F; + border-radius: 4px 4px 4px 4px; + padding-left: 4px; + padding-right: 4px; + height: 15px; } .storygreen { @@ -287,7 +316,7 @@ body { /* Content display {{{ */ .display-audio, .display-file { - width: 500px; + width: 100%; } .display-image { @@ -315,15 +344,16 @@ body { #editform, #editform .editor { display: none; + width: 100% } #codeb { white-space: pre-wrap; } -#editor { +#inplace-editor { display: none; - width: 794px; + width: 100%; height: 800px; font-size: 13px; } diff --git a/static/js/bin.js b/static/js/bin.js index 6e1bbcc..7aed334 100644 --- a/static/js/bin.js +++ b/static/js/bin.js @@ -1,6 +1,6 @@ // @license magnet:?xt=urn:btih:1f739d935676111cfff4b4693e3816e664797050&dn=gpl-3.0.txt GPL-v3-or-Later -var navlist = document.getElementById("info").getElementsByClassName("right")[0]; +var navlist = document.getElementById("info").getElementsByClassName("text-right")[0]; init(); @@ -32,13 +32,13 @@ function edit(ev) { var normalcontent = document.getElementById("normal-content"); normalcontent.removeChild(document.getElementById("normal-code")); - var editordiv = document.getElementById("editor"); + var editordiv = document.getElementById("inplace-editor"); editordiv.style.display = "block"; editordiv.addEventListener('keydown', handleTab); } function paste(ev) { - var editordiv = document.getElementById("editor"); + var editordiv = document.getElementById("inplace-editor"); document.getElementById("newcontent").value = editordiv.value; document.forms["reply"].submit(); } diff --git a/static/js/upload.js b/static/js/upload.js index 5e54d62..159bad2 100644 --- a/static/js/upload.js +++ b/static/js/upload.js @@ -102,8 +102,18 @@ Dropzone.options.dropzone = { previewsContainer: "#uploads", parallelUploads: 5, headers: {"Accept": "application/json"}, - dictDefaultMessage: "Click or Drop file(s)", + dictDefaultMessage: "Click or Drop file(s) or Paste image", dictFallbackMessage: "" }; +document.onpaste = function(event) { + var items = (event.clipboardData || event.originalEvent.clipboardData).items; + for (index in items) { + var item = items[index]; + if (item.kind === "file") { + Dropzone.forElement("#dropzone").addFile(item.getAsFile()); + } + } +}; + // @end-license diff --git a/templates.go b/templates.go index 0687bce..79c90ce 100644 --- a/templates.go +++ b/templates.go @@ -83,6 +83,7 @@ func renderTemplate(tpl *pongo2.Template, context pongo2.Context, r *http.Reques } context["sitepath"] = Config.sitePath + context["selifpath"] = Config.selifPath context["using_auth"] = Config.authFile != "" return tpl.ExecuteWriter(context, writer) diff --git a/templates/404.html b/templates/404.html index c1728e5..3b3d64e 100644 --- a/templates/404.html +++ b/templates/404.html @@ -1,5 +1,7 @@ {% extends "base.html" %} {% block content %} - +
+ +
{% endblock %} diff --git a/templates/API.html b/templates/API.html index 045f4c2..64404b6 100644 --- a/templates/API.html +++ b/templates/API.html @@ -41,6 +41,7 @@

“url”: the publicly available upload url
+ “direct_url”: the url to access the file directly
“filename”: the (optionally generated) filename
“delete_key”: the (optionally generated) deletion key,
“expiry”: the unix timestamp at which the file will expire (0 if never)
@@ -121,6 +122,7 @@ DELETED

“url”: the publicly available upload url
+ “direct_url”: the url to access the file directly
“filename”: the (optionally generated) filename
“expiry”: the unix timestamp at which the file will expire (0 if never)
“size”: the size in bytes of the file
diff --git a/templates/base.html b/templates/base.html index d750cb4..5392f8d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,6 +3,7 @@ {% block title %}{{ sitename }}{% endblock %} + diff --git a/templates/display/audio.html b/templates/display/audio.html index 689bab3..b5ae1e3 100644 --- a/templates/display/audio.html +++ b/templates/display/audio.html @@ -2,8 +2,8 @@ {% block main %} {% endblock %} diff --git a/templates/display/base.html b/templates/display/base.html index 8f33b46..011534c 100644 --- a/templates/display/base.html +++ b/templates/display/base.html @@ -7,22 +7,21 @@ {% block content %}

-
+
{{ filename }}
-
+
{% if expiry %} file expires in {{ expiry }} | {% endif %} {% block infomore %}{% endblock %} {{ size }} | torrent | - get + get
{% block infoleft %}{% endblock %} -
diff --git a/templates/display/bin.html b/templates/display/bin.html index 12e49c9..bd029a2 100644 --- a/templates/display/bin.html +++ b/templates/display/bin.html @@ -12,23 +12,17 @@ {% block infoleft %}
-
+
+ - -
- . + .
@@ -41,7 +35,7 @@ {% block main %}
{{ extra.contents }}
- +
diff --git a/templates/display/file.html b/templates/display/file.html index 5eeb424..670651e 100644 --- a/templates/display/file.html +++ b/templates/display/file.html @@ -2,7 +2,7 @@ {% block main %}
-

You are requesting {{ filename }}, click here to download.

+

You are requesting {{ filename }}, click here to download.

{% if files|length > 0 %}

Contents of the archive:

diff --git a/templates/display/image.html b/templates/display/image.html index b1ea7dd..807b7ad 100644 --- a/templates/display/image.html +++ b/templates/display/image.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% block main %} - - + + {% endblock %} diff --git a/templates/display/pdf.html b/templates/display/pdf.html index 7ad20f1..69501f7 100644 --- a/templates/display/pdf.html +++ b/templates/display/pdf.html @@ -1,10 +1,10 @@ {% extends "base.html" %} {% block main %} - +

It appears your Web browser is not configured to display PDF files. -No worries, just click here to download the PDF file.

+No worries, just click here to download the PDF file.

{% endblock %} diff --git a/templates/display/story.html b/templates/display/story.html index 763fd4a..20e772c 100644 --- a/templates/display/story.html +++ b/templates/display/story.html @@ -10,23 +10,17 @@ {% block infoleft %}
-
+
+ - -
- . + .
@@ -39,7 +33,7 @@ {% block main %}
{% for line in lines %}{% if line|make_list|first == ">" %}{{ line }}{% else %}{{ line }}{% endif %}{% endfor %}
- +
diff --git a/templates/display/video.html b/templates/display/video.html index 9fc90d5..317664b 100644 --- a/templates/display/video.html +++ b/templates/display/video.html @@ -2,7 +2,7 @@ {% block main %} {% endblock %} diff --git a/templates/index.html b/templates/index.html index 5e95d01..d423879 100644 --- a/templates/index.html +++ b/templates/index.html @@ -13,12 +13,13 @@
- Click or Drop file(s) + Click or Drop file(s) or Paste image
+
-
-
diff --git a/templates/paste.html b/templates/paste.html index 9178ba4..7737760 100644 --- a/templates/paste.html +++ b/templates/paste.html @@ -2,24 +2,24 @@ {% block content %}
-
-
- . - -
+
+
+
+ . +
+
+ - -
-
- +
+
diff --git a/upload.go b/upload.go index ee05e80..d46c4d5 100644 --- a/upload.go +++ b/upload.go @@ -306,6 +306,7 @@ func generateBarename() string { func generateJSONresponse(upload Upload, r *http.Request) []byte { js, _ := json.Marshal(map[string]string{ "url": getSiteURL(r) + upload.Filename, + "direct_url": getSiteURL(r) + Config.selifPath + upload.Filename, "filename": upload.Filename, "delete_key": upload.Metadata.DeleteKey, "expiry": strconv.FormatInt(upload.Metadata.Expiry.Unix(), 10),