19 Commits

Author SHA1 Message Date
5ce06d6e6b doc/CHANGELOG: changelog for v1.4.0 2023-05-17 19:27:32 +12:00
0044d7dc77 doc/README: update for latest feature changes 2023-05-17 19:27:21 +12:00
6453700648 preview: better title element 2023-05-17 19:25:45 +12:00
7407353dfb albums: support custom album titles 2023-05-17 19:21:48 +12:00
660038b897 doc/README: update list of endpoints, SDK usage 2023-05-17 19:09:36 +12:00
6cef907cf9 doc/README: alphabetical ordering of options 2023-05-17 19:08:58 +12:00
b959f30882 preview: use browser-side lazy loading for very large galleries 2023-05-17 19:08:36 +12:00
64b900d90c server: add readonly mode to block uploads 2023-05-17 19:08:24 +12:00
e26a3b58b0 upload: return error if the short IDs collide for whatever reason 2023-05-17 18:54:30 +12:00
4294738337 albums: basic support (json array using contented/album mime-type) 2023-05-17 18:52:13 +12:00
156e2ab540 thumbnail: if the request is cancelled, don't wait for the semaphore 2023-05-17 18:18:02 +12:00
ad56309cb0 sdk: replace all the early-load logic with promises 2023-05-17 18:09:28 +12:00
f13618fef1 sdk: inline the widget html definition 2023-05-17 18:09:15 +12:00
e8dd95a830 sdk: drop polyfills, use jquery 3 2023-05-17 17:38:32 +12:00
bf3339fec7 sdk: flatten out some nested callbacks 2023-05-17 17:34:22 +12:00
cb29cf83ac server: replace go-bindata with stdlib embed.FS 2023-05-17 17:24:47 +12:00
ad95ac219d mod: declare go1.19 (debian bookworm) 2023-05-17 17:24:23 +12:00
11003e010d vendor: update bolt v1.3.1 -> etcd.io/bbolt v1.3.7 2023-05-17 17:24:00 +12:00
6b4c2cc208 makefile: remove win32 and src targets 2023-05-17 17:22:47 +12:00
16 changed files with 773 additions and 1108 deletions

View File

@@ -21,22 +21,12 @@ all: build/linux64/contented build/win32/contented.exe
dist: \ dist: \
_dist/contented-$(VERSION)-linux64.tar.gz \ _dist/contented-$(VERSION)-linux64.tar.gz \
_dist/contented-$(VERSION)-win32.7z \
_dist/contented-$(VERSION)-src.zip _dist/contented-$(VERSION)-src.zip
clean: clean:
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
if [ -d ./build ] ; then rm -r ./build ; fi if [ -d ./build ] ; then rm -r ./build ; fi
if [ -f ./contented ] ; then rm ./contented ; fi if [ -f ./contented ] ; then rm ./contented ; fi
#
# Generated files
#
staticResources.go: static/ static/*
go-bindata -o staticResources.go -prefix static -pkg contented static
# #
# Release artefacts # Release artefacts
# #
@@ -48,26 +38,6 @@ build/linux64/contented: $(SOURCES) staticResources.go
go build $(GOFLAGS) -o ../../build/linux64/contented \ go build $(GOFLAGS) -o ../../build/linux64/contented \
) )
build/win32/contented.exe: $(SOURCES) staticResources.go
mkdir -p build/win32
(cd cmd/contented ; \
PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \
CGO_ENABLED=1 GOOS=windows GOARCH=386 \
go build $(GOFLAGS) -o ../../build/win32/contented.exe \
)
_dist/contented-$(VERSION)-linux64.tar.gz: build/linux64/contented _dist/contented-$(VERSION)-linux64.tar.gz: build/linux64/contented
mkdir -p _dist mkdir -p _dist
tar caf _dist/contented-$(VERSION)-linux64.tar.gz -C build/linux64 contented --owner=0 --group=0 tar caf _dist/contented-$(VERSION)-linux64.tar.gz -C build/linux64 contented --owner=0 --group=0
_dist/contented-$(VERSION)-win32.7z: build/win32/contented.exe
mkdir -p _dist
( cd build/win32 ; \
if [ -f dist.7z ] ; then rm dist.7z ; fi ; \
7z a dist.7z contented.exe ; \
mv dist.7z ../../_dist/contented-$(VERSION)-win32.7z \
)
_dist/contented-$(VERSION)-src.zip: $(SOURCES)
git archive HEAD -o _dist/contented-$(VERSION)-src.zip

View File

@@ -3,13 +3,14 @@ package contented
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
"time" "time"
"github.com/boltdb/bolt"
"github.com/speps/go-hashids" "github.com/speps/go-hashids"
bolt "go.etcd.io/bbolt"
) )
const ( const (
@@ -62,9 +63,17 @@ func (this *Server) AddMetadata(m Metadata) (string, error) {
err = this.db.Update(func(tx *bolt.Tx) error { err = this.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket(this.metadataBucket) b := tx.Bucket(this.metadataBucket)
seq, _ := b.NextSequence() // cannot fail seq, err := b.NextSequence()
if err != nil {
return fmt.Errorf("NextSequence: %w", err)
}
shortRef = idToString(seq) shortRef = idToString(seq)
return tx.Bucket(this.metadataBucket).Put([]byte(shortRef), jb)
if b.Get([]byte(shortRef)) != nil {
return fmt.Errorf("Next bucket sequence %d creates colliding ID %s", seq, shortRef)
}
return b.Put([]byte(shortRef), jb)
}) })
if err != nil { if err != nil {
return "", err return "", err

View File

@@ -6,6 +6,8 @@ A file / image / paste upload server with a focus on embedding.
You can use contented as a standalone upload server, or you can use the SDK to embed its upload widget into another website. You can use contented as a standalone upload server, or you can use the SDK to embed its upload widget into another website.
The name is a pun on "content" and the -d suffix for server daemons.
## Features ## Features
- Drag and drop upload - Drag and drop upload
@@ -13,6 +15,7 @@ You can use contented as a standalone upload server, or you can use the SDK to e
- Pastebin upload - Pastebin upload
- Custom drawing upload ([via drawingboard.js](https://github.com/Leimi/drawingboard.js)) - Custom drawing upload ([via drawingboard.js](https://github.com/Leimi/drawingboard.js))
- Ctrl-V upload - Ctrl-V upload
- Galleries
- SDK-oriented design for embedding, including CORS support - SDK-oriented design for embedding, including CORS support
- Mobile friendly HTML interface - Mobile friendly HTML interface
- Preserves uploaded filename and content-type metadata - Preserves uploaded filename and content-type metadata
@@ -26,14 +29,18 @@ You can use contented as a standalone upload server, or you can use the SDK to e
``` ```
Usage of contented: Usage of contented:
-concurrentthumbs int
Simultaneous thumbnail generation (default 16)
-data string -data string
Directory for stored content (default "") Directory for stored content (default ".")
-db string -db string
Path for metadata database (default "contented.db") Path for metadata database (default "contented.db")
-diskFilesWorldReadable -diskFilesWorldReadable
Save files as 0644 instead of 0600 Save files as 0644 instead of 0600
-enableHomepage -enableHomepage
Enable homepage (disable for embedded use only) (default true) Enable homepage (disable for embedded use only) (default true)
-enableUpload
Enable uploads (disable for read-only mode) (default true)
-listen string -listen string
IP/Port to bind server (default "127.0.0.1:80") IP/Port to bind server (default "127.0.0.1:80")
-max int -max int
@@ -44,8 +51,6 @@ Usage of contented:
Title used in web interface (default "contented") Title used in web interface (default "contented")
-trustXForwardedFor -trustXForwardedFor
Trust X-Forwarded-For reverse proxy headers Trust X-Forwarded-For reverse proxy headers
-concurrentthumbs
Simultaneous thumbnail generation (default 16)
``` ```
If you are hosting behind a reverse proxy, remember to set its post body size parameter appropriately (e.g. `client_max_body_size` for nginx). If you are hosting behind a reverse proxy, remember to set its post body size parameter appropriately (e.g. `client_max_body_size` for nginx).
@@ -57,21 +62,39 @@ The server responds on the following URLs:
URL |Method |Description URL |Method |Description
---------------------|-------|--- ---------------------|-------|---
`/get/{ID}` |`GET` |Download item content `/get/{ID}` |`GET` |Download item content
`/p/{ID}` |`GET` |Preview item content (HTML)
`/p/{ID}-{ID}-...` |`GET` |Preview multiple item content (HTML)
`/info/{ID}` |`GET` |Get item content metadata (JSON) `/info/{ID}` |`GET` |Get item content metadata (JSON)
`/thumb/{Type}/{ID}` |`GET` |Get item thumbnail image `/thumb/{Type}/{ID}` |`GET` |Get item thumbnail image (JPEG). "Type" should match `[sbtmlh]`.
`/about` |`GET` |Get server metadata (JSON) `/about` |`GET` |Get server metadata (JSON)
## Usage (Embedding for web) ## Usage (Embedding for web)
Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element. Your callback will be passed an array of file IDs of any uploaded items. Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element:
```html ```html
<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script> <script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
contented.init("#target", function(/* String[] */ items) {}); contented.init("#target");
``` ```
You can optionally supply additional ordered parameters to `contented.init`:
1. A callback, that will be passed an array of file IDs of any uploaded items
2. A callback, that will be called if the SDK widget is closed
## Changelog ## Changelog
2023-05-17: 1.4.0
- BREAKING: Remove support for some old web browsers (require jQuery 3, ES6 template literals, Promises, Canvas.toBlob)
- Feature: Initial album support with custom titles
- Feature: Support readonly mode
- Enhancement: Use lazy-loading for large image galleries
- Enhancement: Better tab titles on preview pages
- Fix an issue with continuing server-side thumbnailing work even if the http client has gone away
- Fix an issue with not warning on colliding hashIDs
- Internal: Refactor the SDK's initialization phase
- Internal: Update bolt library dependency
2020-07-25: 1.3.1 2020-07-25: 1.3.1
- Fix an issue with dependencies causing failure to compile in Modules mode - Fix an issue with dependencies causing failure to compile in Modules mode

View File

@@ -1,8 +1,9 @@
package contented package contented
import ( import (
"bytes" "embed"
"encoding/json" "encoding/json"
"io/fs"
"log" "log"
"net/http" "net/http"
"os" "os"
@@ -10,14 +11,19 @@ import (
"strings" "strings"
"time" "time"
"github.com/boltdb/bolt"
"github.com/mxk/go-flowrate/flowrate" "github.com/mxk/go-flowrate/flowrate"
bolt "go.etcd.io/bbolt"
) )
//go:embed static
var staticAssets embed.FS
var SERVER_HEADER string = `contented/0.0.0-dev` var SERVER_HEADER string = `contented/0.0.0-dev`
const DEFAULT_MAX_CONCURRENT_THUMBS = 16 const DEFAULT_MAX_CONCURRENT_THUMBS = 16
const ALBUM_MIMETYPE = `contented/album`
type ServerPublicProperties struct { type ServerPublicProperties struct {
AppTitle string AppTitle string
MaxUploadBytes int64 MaxUploadBytes int64
@@ -31,6 +37,7 @@ type ServerOptions struct {
BandwidthLimit int64 BandwidthLimit int64
TrustXForwardedFor bool TrustXForwardedFor bool
EnableHomepage bool EnableHomepage bool
EnableUpload bool
MaxConcurrentThumbs int MaxConcurrentThumbs int
ServerPublicProperties ServerPublicProperties
} }
@@ -49,6 +56,7 @@ type Server struct {
startTime time.Time startTime time.Time
thumbnailSem chan struct{} thumbnailSem chan struct{}
metadataBucket []byte metadataBucket []byte
staticDir fs.FS // interface
} }
func NewServer(opts *ServerOptions) (*Server, error) { func NewServer(opts *ServerOptions) (*Server, error) {
@@ -63,6 +71,8 @@ func NewServer(opts *ServerOptions) (*Server, error) {
log.Printf("Allowing %d concurrent thumbnails", s.opts.MaxConcurrentThumbs) log.Printf("Allowing %d concurrent thumbnails", s.opts.MaxConcurrentThumbs)
} }
s.staticDir, _ = fs.Sub(staticAssets, `static`) // can't fail
// "fill" the thumbnailer semaphore // "fill" the thumbnailer semaphore
s.thumbnailSem = make(chan struct{}, s.opts.MaxConcurrentThumbs) s.thumbnailSem = make(chan struct{}, s.opts.MaxConcurrentThumbs)
for i := 0; i < s.opts.MaxConcurrentThumbs; i += 1 { for i := 0; i < s.opts.MaxConcurrentThumbs; i += 1 {
@@ -157,11 +167,17 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Blanket allow (headers already set) // Blanket allow (headers already set)
w.WriteHeader(200) w.WriteHeader(200)
} else if r.Method == "GET" && r.URL.Path == `/` && this.opts.EnableHomepage { } else if r.Method == "GET" {
http.Redirect(w, r, `/index.html`, http.StatusFound)
} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" && (this.opts.EnableHomepage || r.URL.Path != `/index.html`) { // Conditionally block homepage access
http.ServeContent(w, r, r.URL.Path[1:], this.startTime, bytes.NewReader(static)) if !this.opts.EnableHomepage && (r.URL.Path == `/index.html` || r.URL.Path == `/`) {
http.Error(w, "Not found", 404)
return
}
// Serve static html/css/js assets
// http.FileServer transparently redirects index.html->/ internally
http.FileServer(http.FS(this.staticDir)).ServeHTTP(w, r)
} else { } else {
http.Error(w, "Not found", 404) http.Error(w, "Not found", 404)

View File

@@ -20,6 +20,7 @@ func main() {
maxUploadSpeed := flag.Int("speed", 0, "Maximum upload speed in bytes/sec (set zero for unlimited)") maxUploadSpeed := flag.Int("speed", 0, "Maximum upload speed in bytes/sec (set zero for unlimited)")
trustXForwardedFor := flag.Bool("trustXForwardedFor", false, "Trust X-Forwarded-For reverse proxy headers") trustXForwardedFor := flag.Bool("trustXForwardedFor", false, "Trust X-Forwarded-For reverse proxy headers")
enableHomepage := flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)") enableHomepage := flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)")
enableUpload := flag.Bool("enableUpload", true, "Enable uploads (disable for read-only mode)")
diskFilesWorldReadable := flag.Bool("diskFilesWorldReadable", false, "Save files as 0644 instead of 0600") diskFilesWorldReadable := flag.Bool("diskFilesWorldReadable", false, "Save files as 0644 instead of 0600")
maxConcurrentThumbs := flag.Int("concurrentthumbs", contented.DEFAULT_MAX_CONCURRENT_THUMBS, "Simultaneous thumbnail generation") maxConcurrentThumbs := flag.Int("concurrentthumbs", contented.DEFAULT_MAX_CONCURRENT_THUMBS, "Simultaneous thumbnail generation")
@@ -31,6 +32,7 @@ func main() {
BandwidthLimit: int64(*maxUploadSpeed), BandwidthLimit: int64(*maxUploadSpeed),
TrustXForwardedFor: *trustXForwardedFor, TrustXForwardedFor: *trustXForwardedFor,
EnableHomepage: *enableHomepage, EnableHomepage: *enableHomepage,
EnableUpload: *enableUpload,
DiskFilesWorldReadable: *diskFilesWorldReadable, DiskFilesWorldReadable: *diskFilesWorldReadable,
MaxConcurrentThumbs: *maxConcurrentThumbs, MaxConcurrentThumbs: *maxConcurrentThumbs,
ServerPublicProperties: contented.ServerPublicProperties{ ServerPublicProperties: contented.ServerPublicProperties{

12
go.mod
View File

@@ -2,10 +2,16 @@ module code.ivysaur.me/contented
require ( require (
code.ivysaur.me/thumbnail v1.0.2 code.ivysaur.me/thumbnail v1.0.2
github.com/boltdb/bolt v1.3.1
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
github.com/speps/go-hashids v1.0.0 github.com/speps/go-hashids v1.0.0
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b // indirect go.etcd.io/bbolt v1.3.7
) )
go 1.13 require (
code.ivysaur.me/imagequant/v2 v2.12.6 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
golang.org/x/image v0.0.0-20200618115811-c13761719519 // indirect
golang.org/x/sys v0.4.0 // indirect
)
go 1.19

4
go.sum
View File

@@ -10,8 +10,12 @@ github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/speps/go-hashids v1.0.0 h1:jdFC07PrExRM4Og5Ev4411Tox75aFpkC77NlmutadNI= github.com/speps/go-hashids v1.0.0 h1:jdFC07PrExRM4Og5Ev4411Tox75aFpkC77NlmutadNI=
github.com/speps/go-hashids v1.0.0/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc= github.com/speps/go-hashids v1.0.0/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34= golang.org/x/image v0.0.0-20200618115811-c13761719519 h1:1e2ufUJNM3lCHEY5jIgac/7UTjd6cgJNdatjPdFWf34=
golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b h1:5rOiLYVqtE+JehJPVJTXQJaP8aT3cpJC1Iy22+5WLFU= golang.org/x/sys v0.0.0-20180606202747-9527bec2660b h1:5rOiLYVqtE+JehJPVJTXQJaP8aT3cpJC1Iy22+5WLFU=
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=

View File

@@ -1,11 +1,13 @@
package contented package contented
import ( import (
"encoding/json"
"fmt" "fmt"
"html" "html"
"log" "log"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
) )
@@ -14,10 +16,30 @@ func (this *Server) handlePreview(w http.ResponseWriter, fileIDList string) {
fileIDs := strings.Split(fileIDList, `-`) fileIDs := strings.Split(fileIDList, `-`)
// Early get metadata for the first listed element
specialTitle := ""
if len(fileIDs) == 1 {
mFirst, err := this.Metadata(fileIDs[0])
if err != nil { // Same error handling as below -
if os.IsNotExist(err) {
http.Error(w, "Not found", 404)
return
}
log.Println(err.Error())
http.Error(w, "Internal error", 500)
return
}
specialTitle = mFirst.Filename + " (" + fileIDs[0] + ")"
} else {
specialTitle = fmt.Sprintf("%d images", len(fileIDs))
}
tmpl := `<!DOCTYPE html> tmpl := `<!DOCTYPE html>
<html prefix="og: http://ogp.me/ns#"> <html prefix="og: http://ogp.me/ns#">
<head> <head>
<title>` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `</title> <title>` + html.EscapeString(specialTitle+" | "+this.opts.ServerPublicProperties.AppTitle) + `</title>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta property="og:title" content="` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `" /> <meta property="og:title" content="` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `" />
<meta property="og:site_name" content="` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `" /> <meta property="og:site_name" content="` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `" />
@@ -56,6 +78,19 @@ html, body {
line-height: 0; line-height: 0;
width: 340px; width: 340px;
text-align: center; text-align: center;
position: relative;
}
.thumbnail-overlay {
position: absolute;
bottom: 4px;
right: 4px;
padding: 4px 8px;
line-height: 1.5em;
pointer-events: none;
background: red;
color: white;
} }
.properties { .properties {
background: #000; background: #000;
@@ -82,21 +117,64 @@ html, body {
return return
} }
tmpl += ` if m.MimeType == ALBUM_MIMETYPE {
<div class="entry"> // Special handling for albums
<div class="thumbnail"> f, err := os.Open(filepath.Join(this.opts.DataDirectory, m.FileHash))
<a href="` + html.EscapeString(`/get/`+fileID) + `"><img src="` + html.EscapeString(`/thumb/m/`+fileID) + `"></a> if err != nil {
log.Printf("Opening file '%s' for preview of album '%s': %s", m.FileHash, fileID, err.Error())
http.Error(w, "Internal error", 500)
return
}
var childIDs []string
err = json.NewDecoder(f).Decode(&childIDs)
f.Close()
if err != nil {
log.Printf("Failed to parse album '%s': %s", fileID, err)
http.Error(w, "Internal error", 500)
return
}
if len(childIDs) == 0 {
log.Printf("Failed to parse album '%s': no entries in album", fileID)
http.Error(w, "Internal error", 500)
return
}
tmpl += `
<div class="entry">
<div class="thumbnail">
<a href="` + html.EscapeString(`/p/`+strings.Join(childIDs, `-`)) + `"><img loading="lazy" src="` + html.EscapeString(`/thumb/m/`+childIDs[0]) + `"></a>
<div class="thumbnail-overlay">` + fmt.Sprintf("%d", len(childIDs)) + ` image(s)</div>
</div>
<div class="properties">
<b>Name:</b> ` + html.EscapeString(m.Filename) + `<br>
<b>Hash:</b> <span title="` + html.EscapeString(m.FileHash) + `">hover</span><br>
<b>File type:</b> Album<br>
<b>Size:</b> ` + fmt.Sprintf("%d", len(childIDs)) + ` image(s)<br>
<b>Uploader:</b> ` + html.EscapeString(m.UploadIP) + `<br>
<b>Uploaded at:</b> ` + html.EscapeString(m.UploadTime.Format(time.RFC3339)) + `<br>
</div>
</div> </div>
<div class="properties"> `
<b>Name:</b> ` + html.EscapeString(m.Filename) + `<br>
<b>Hash:</b> <span title="` + html.EscapeString(m.FileHash) + `">hover</span><br> } else {
<b>File type:</b> ` + html.EscapeString(m.MimeType) + `<br> tmpl += `
<b>Size:</b> ` + html.EscapeString(fmt.Sprintf("%d", m.FileSize)) + `<br> <div class="entry">
<b>Uploader:</b> ` + html.EscapeString(m.UploadIP) + `<br> <div class="thumbnail">
<b>Uploaded at:</b> ` + html.EscapeString(m.UploadTime.Format(time.RFC3339)) + `<br> <a href="` + html.EscapeString(`/get/`+fileID) + `"><img loading="lazy" src="` + html.EscapeString(`/thumb/m/`+fileID) + `"></a>
</div>
<div class="properties">
<b>Name:</b> ` + html.EscapeString(m.Filename) + `<br>
<b>Hash:</b> <span title="` + html.EscapeString(m.FileHash) + `">hover</span><br>
<b>File type:</b> ` + html.EscapeString(m.MimeType) + `<br>
<b>Size:</b> ` + html.EscapeString(fmt.Sprintf("%d", m.FileSize)) + `<br>
<b>Uploader:</b> ` + html.EscapeString(m.UploadIP) + `<br>
<b>Uploaded at:</b> ` + html.EscapeString(m.UploadTime.Format(time.RFC3339)) + `<br>
</div>
</div> </div>
</div> `
` }
} }
if this.opts.EnableHomepage { if this.opts.EnableHomepage {

View File

@@ -38,7 +38,7 @@ button.again {
</div> </div>
</div> </div>
<script type="text/javascript" src="/jquery-1.12.4.min.js"></script> <script type="text/javascript" src="/jquery-3.7.0.min.js"></script>
<script type="text/javascript" src="/sdk.js"></script> <script type="text/javascript" src="/sdk.js"></script>
<script type="text/javascript"> <script type="text/javascript">
"use strict"; "use strict";

File diff suppressed because one or more lines are too long

2
static/jquery-3.7.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -1,161 +0,0 @@
<style type="text/css">
.contented {
box-sizing:border-box;
text-align: center;
border: 8px dashed lightgrey;
padding: 12px;
background:white; /* not transparent */
text-overflow:hidden;
overflow:auto;
width:100%;
height:100%;
position:relative;
}
.contented .contented-close {
position: absolute;
top: 12px;
right: 12px;
width: 24px;
height: 24px;
cursor: pointer;
}
.contented .contented-upload-type-selector {
display:block;
margin-bottom: 1em;
-webkit-user-select: none;
user-select: none;
}
.contented .contented-upload-type {
display:inline-block;
opacity:0.2;
transition:opacity linear 0.1s;
cursor:pointer;
}
.contented .contented-upload-type:hover {
opacity:0.5;
transition:opacity linear 0s;
}
.contented .contented-upload-type svg {
width:36px;
height:36px;
}
.contented .contented-upload-type.contented-upload-type-active {
opacity:1;
}
.contented.is-dragging {
background: lightblue;
}
.contented-content-area {
position:absolute;
top: 60px;
bottom: 10px;
left: 10px;
width: calc(100% - 20px);
/* Prevent blur under translateY */
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
transform-style: preserve-3d;
}
.contented-content-area > div {
position: relative;
top: 50%;
transform: translateY(-50%);
}
.contented-upload-if {
display:none;
}
.contented-if-paste, .contented-if-drawing {
height:100%;
}
.contented-upload-if.contented-active {
display:block;
}
.contented textarea {
resize: none;
width:100%;
height:calc(100% - 1em - 15px);
box-sizing:border-box;
}
.contented-progress-bar {
display: block;
width:90%;
margin:0.5em auto 0 auto;
height:16px;
border-radius:8px;
background:lightgrey;
position:relative;
overflow:hidden;
}
.contented-progress-element {
position:absolute;
background:darkgreen;
left:0;
width:0%;
height:100%;
}
</style>
<div class="contented">
<div class="contented-close">
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
</div>
<div class="contented-upload-type-selector">
<div class="contented-upload-type contented-upload-type-active" data-upload-type="drag" title="Drag and drop">
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"></path>
</svg>
</div>
<div class="contented-upload-type" data-upload-type="file" title="Multiple files">
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z"></path>
</svg>
</div>
<div class="contented-upload-type" data-upload-type="paste" title="Paste">
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z" />
</svg>
</div>
<div class="contented-upload-type" data-upload-type="drawing" title="Drawing">
<svg viewBox="0 0 24 24">
<path fill="#000000" d="M16.84,2.73C16.45,2.73 16.07,2.88 15.77,3.17L13.65,5.29L18.95,10.6L21.07,8.5C21.67,7.89 21.67,6.94 21.07,6.36L17.9,3.17C17.6,2.88 17.22,2.73 16.84,2.73M12.94,6L4.84,14.11L7.4,14.39L7.58,16.68L9.86,16.85L10.15,19.41L18.25,11.3M4.25,15.04L2.5,21.73L9.2,19.94L8.96,17.78L6.65,17.61L6.47,15.29" />
</svg>
</div>
</div>
<div class="contented-content-area">
<div class="contented-upload-if contented-if-drag contented-active">
<label>Drop files or Ctrl-V to upload <span class="contented-extratext"></span></label>
</div>
<div class="contented-upload-if contented-if-file">
<label>Select files to upload <span class="contented-extratext"></span></label><br>
<input class="contented-file-selector" type="file" multiple>
<button class="contented-file-upload">Upload &raquo;</button>
</div>
<div class="contented-upload-if contented-if-paste">
<textarea placeholder="Paste content here"></textarea>
<button class="contented-paste-upload">Upload &raquo;</button>
</div>
<div class="contented-upload-if contented-if-drawing">
<div class="contented-drawing-area"></div>
</div>
<div class="contented-upload-if contented-if-progress">
<label>...</label>
<div class="contented-progress-bar"><div class="contented-progress-element"></div></div>
</div>
</div>
</div>

File diff suppressed because one or more lines are too long

View File

@@ -62,7 +62,12 @@ func (this *Server) handleThumb(w http.ResponseWriter, r *http.Request, thumbnai
} }
// Only a limited number of thumbnails can be generated concurrently // Only a limited number of thumbnails can be generated concurrently
<-this.thumbnailSem select {
case <-this.thumbnailSem:
case <-r.Context().Done():
http.Error(w, r.Context().Err().Error(), 400) // probably won't be delivered anyway
return
}
defer func() { this.thumbnailSem <- struct{}{} }() defer func() { this.thumbnailSem <- struct{}{} }()
if ctx.Err() != nil { if ctx.Err() != nil {

View File

@@ -18,6 +18,11 @@ import (
func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) { func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
if !this.opts.EnableUpload {
http.Error(w, "Server is read-only", 403)
return
}
remoteIP := this.remoteIP(r) remoteIP := this.remoteIP(r)
err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory