Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce06d6e6b | |||
| 0044d7dc77 | |||
| 6453700648 | |||
| 7407353dfb | |||
| 660038b897 | |||
| 6cef907cf9 | |||
| b959f30882 | |||
| 64b900d90c | |||
| e26a3b58b0 | |||
| 4294738337 | |||
| 156e2ab540 | |||
| ad56309cb0 | |||
| f13618fef1 | |||
| e8dd95a830 | |||
| bf3339fec7 | |||
| cb29cf83ac | |||
| ad95ac219d | |||
| 11003e010d | |||
| 6b4c2cc208 |
30
Makefile
30
Makefile
@@ -21,22 +21,12 @@ all: build/linux64/contented build/win32/contented.exe
|
||||
|
||||
dist: \
|
||||
_dist/contented-$(VERSION)-linux64.tar.gz \
|
||||
_dist/contented-$(VERSION)-win32.7z \
|
||||
_dist/contented-$(VERSION)-src.zip
|
||||
|
||||
clean:
|
||||
if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
|
||||
if [ -d ./build ] ; then rm -r ./build ; 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
|
||||
#
|
||||
@@ -48,26 +38,6 @@ build/linux64/contented: $(SOURCES) staticResources.go
|
||||
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
|
||||
mkdir -p _dist
|
||||
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
|
||||
|
||||
|
||||
15
Metadata.go
15
Metadata.go
@@ -3,13 +3,14 @@ package contented
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/speps/go-hashids"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -62,9 +63,17 @@ func (this *Server) AddMetadata(m Metadata) (string, error) {
|
||||
|
||||
err = this.db.Update(func(tx *bolt.Tx) error {
|
||||
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)
|
||||
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 {
|
||||
return "", err
|
||||
|
||||
35
README.md
35
README.md
@@ -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.
|
||||
|
||||
The name is a pun on "content" and the -d suffix for server daemons.
|
||||
|
||||
## Features
|
||||
|
||||
- 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
|
||||
- Custom drawing upload ([via drawingboard.js](https://github.com/Leimi/drawingboard.js))
|
||||
- Ctrl-V upload
|
||||
- Galleries
|
||||
- SDK-oriented design for embedding, including CORS support
|
||||
- Mobile friendly HTML interface
|
||||
- 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:
|
||||
-concurrentthumbs int
|
||||
Simultaneous thumbnail generation (default 16)
|
||||
-data string
|
||||
Directory for stored content (default "")
|
||||
Directory for stored content (default ".")
|
||||
-db string
|
||||
Path for metadata database (default "contented.db")
|
||||
-diskFilesWorldReadable
|
||||
Save files as 0644 instead of 0600
|
||||
-enableHomepage
|
||||
Enable homepage (disable for embedded use only) (default true)
|
||||
-enableUpload
|
||||
Enable uploads (disable for read-only mode) (default true)
|
||||
-listen string
|
||||
IP/Port to bind server (default "127.0.0.1:80")
|
||||
-max int
|
||||
@@ -44,8 +51,6 @@ Usage of contented:
|
||||
Title used in web interface (default "contented")
|
||||
-trustXForwardedFor
|
||||
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).
|
||||
@@ -57,21 +62,39 @@ The server responds on the following URLs:
|
||||
URL |Method |Description
|
||||
---------------------|-------|---
|
||||
`/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)
|
||||
`/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)
|
||||
|
||||
## 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
|
||||
<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
|
||||
|
||||
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
|
||||
- Fix an issue with dependencies causing failure to compile in Modules mode
|
||||
|
||||
|
||||
28
Server.go
28
Server.go
@@ -1,8 +1,9 @@
|
||||
package contented
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -10,14 +11,19 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
"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`
|
||||
|
||||
const DEFAULT_MAX_CONCURRENT_THUMBS = 16
|
||||
|
||||
const ALBUM_MIMETYPE = `contented/album`
|
||||
|
||||
type ServerPublicProperties struct {
|
||||
AppTitle string
|
||||
MaxUploadBytes int64
|
||||
@@ -31,6 +37,7 @@ type ServerOptions struct {
|
||||
BandwidthLimit int64
|
||||
TrustXForwardedFor bool
|
||||
EnableHomepage bool
|
||||
EnableUpload bool
|
||||
MaxConcurrentThumbs int
|
||||
ServerPublicProperties
|
||||
}
|
||||
@@ -49,6 +56,7 @@ type Server struct {
|
||||
startTime time.Time
|
||||
thumbnailSem chan struct{}
|
||||
metadataBucket []byte
|
||||
staticDir fs.FS // interface
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
s.staticDir, _ = fs.Sub(staticAssets, `static`) // can't fail
|
||||
|
||||
// "fill" the thumbnailer semaphore
|
||||
s.thumbnailSem = make(chan struct{}, s.opts.MaxConcurrentThumbs)
|
||||
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)
|
||||
w.WriteHeader(200)
|
||||
|
||||
} else if r.Method == "GET" && r.URL.Path == `/` && this.opts.EnableHomepage {
|
||||
http.Redirect(w, r, `/index.html`, http.StatusFound)
|
||||
} else if r.Method == "GET" {
|
||||
|
||||
} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" && (this.opts.EnableHomepage || r.URL.Path != `/index.html`) {
|
||||
http.ServeContent(w, r, r.URL.Path[1:], this.startTime, bytes.NewReader(static))
|
||||
// Conditionally block homepage access
|
||||
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 {
|
||||
http.Error(w, "Not found", 404)
|
||||
|
||||
@@ -20,6 +20,7 @@ func main() {
|
||||
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")
|
||||
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")
|
||||
maxConcurrentThumbs := flag.Int("concurrentthumbs", contented.DEFAULT_MAX_CONCURRENT_THUMBS, "Simultaneous thumbnail generation")
|
||||
|
||||
@@ -31,6 +32,7 @@ func main() {
|
||||
BandwidthLimit: int64(*maxUploadSpeed),
|
||||
TrustXForwardedFor: *trustXForwardedFor,
|
||||
EnableHomepage: *enableHomepage,
|
||||
EnableUpload: *enableUpload,
|
||||
DiskFilesWorldReadable: *diskFilesWorldReadable,
|
||||
MaxConcurrentThumbs: *maxConcurrentThumbs,
|
||||
ServerPublicProperties: contented.ServerPublicProperties{
|
||||
|
||||
12
go.mod
12
go.mod
@@ -2,10 +2,16 @@ module code.ivysaur.me/contented
|
||||
|
||||
require (
|
||||
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/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
4
go.sum
@@ -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/speps/go-hashids v1.0.0 h1:jdFC07PrExRM4Og5Ev4411Tox75aFpkC77NlmutadNI=
|
||||
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/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/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=
|
||||
|
||||
106
preview.go
106
preview.go
@@ -1,11 +1,13 @@
|
||||
package contented
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -14,10 +16,30 @@ func (this *Server) handlePreview(w http.ResponseWriter, fileIDList string) {
|
||||
|
||||
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>
|
||||
<html prefix="og: http://ogp.me/ns#">
|
||||
<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 property="og:title" 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;
|
||||
width: 340px;
|
||||
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 {
|
||||
background: #000;
|
||||
@@ -82,21 +117,64 @@ html, body {
|
||||
return
|
||||
}
|
||||
|
||||
tmpl += `
|
||||
<div class="entry">
|
||||
<div class="thumbnail">
|
||||
<a href="` + html.EscapeString(`/get/`+fileID) + `"><img src="` + html.EscapeString(`/thumb/m/`+fileID) + `"></a>
|
||||
if m.MimeType == ALBUM_MIMETYPE {
|
||||
// Special handling for albums
|
||||
f, err := os.Open(filepath.Join(this.opts.DataDirectory, m.FileHash))
|
||||
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 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>
|
||||
`
|
||||
|
||||
} else {
|
||||
tmpl += `
|
||||
<div class="entry">
|
||||
<div class="thumbnail">
|
||||
<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>
|
||||
`
|
||||
`
|
||||
}
|
||||
}
|
||||
|
||||
if this.opts.EnableHomepage {
|
||||
|
||||
@@ -38,7 +38,7 @@ button.again {
|
||||
</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">
|
||||
"use strict";
|
||||
|
||||
5
static/jquery-1.12.4.min.js
vendored
5
static/jquery-1.12.4.min.js
vendored
File diff suppressed because one or more lines are too long
2
static/jquery-3.7.0.min.js
vendored
Normal file
2
static/jquery-3.7.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1002
static/sdk.js
1002
static/sdk.js
File diff suppressed because it is too large
Load Diff
@@ -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 »</button>
|
||||
</div>
|
||||
|
||||
<div class="contented-upload-if contented-if-paste">
|
||||
<textarea placeholder="Paste content here"></textarea>
|
||||
<button class="contented-paste-upload">Upload »</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
7
thumb.go
7
thumb.go
@@ -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
|
||||
<-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{}{} }()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
|
||||
@@ -18,6 +18,11 @@ import (
|
||||
|
||||
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)
|
||||
|
||||
err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory
|
||||
|
||||
Reference in New Issue
Block a user