Compare commits
	
		
			92 Commits
		
	
	
		
			v1.1.0-rc1
			...
			v1.4.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5ce06d6e6b | |||
| 0044d7dc77 | |||
| 6453700648 | |||
| 7407353dfb | |||
| 660038b897 | |||
| 6cef907cf9 | |||
| b959f30882 | |||
| 64b900d90c | |||
| e26a3b58b0 | |||
| 4294738337 | |||
| 156e2ab540 | |||
| ad56309cb0 | |||
| f13618fef1 | |||
| e8dd95a830 | |||
| bf3339fec7 | |||
| cb29cf83ac | |||
| ad95ac219d | |||
| 11003e010d | |||
| 6b4c2cc208 | |||
| ccc1f73e52 | |||
| b48d1347c7 | |||
| bed8a88b09 | |||
| 0a70d99af5 | |||
| 6720cbc0d9 | |||
| 8d11b7c434 | |||
| 147608a327 | |||
| 951c87b63f | |||
| 25180afaa2 | |||
| 33ca03b9d7 | |||
| b562ca4bd4 | |||
| a25dc5827b | |||
| 5263f96956 | |||
| 2b2add62b2 | |||
| fdc93e6485 | |||
| b08f1c33d5 | |||
| 524f37d9fe | |||
| c958c57794 | |||
| 8fbad2a1e0 | |||
| 989cd195f8 | |||
| cb4933fa72 | |||
| f504ab5929 | |||
| f8e95a8037 | |||
| feaa51cfcd | |||
| ddf76aacc5 | |||
| ca8a0d55ba | |||
| fe4aace777 | |||
| be864235cb | |||
| dba699524f | |||
| 7950ca3004 | |||
| 6e5ca0c61c | |||
| 7255cd03cb | |||
| 3cf4986418 | |||
| 0a338c4568 | |||
| 0fbf401fd3 | |||
| 48ca68fafd | |||
| f3e594d307 | |||
| 396672b02b | |||
| d25b867e90 | |||
| cd60e4c855 | |||
| ee72f188a2 | |||
| 30f5b40e1d | |||
| 3a103ae484 | |||
| 08321818ff | |||
| 14456e6539 | |||
| f27d14aac4 | |||
| 6ab2b08099 | |||
| 930869759b | |||
| d35c81ed21 | |||
| 139117b4d5 | |||
| b88273ec64 | |||
| 79cb8733e5 | |||
| 366c307e02 | |||
| 23ad509f33 | |||
| a6e495f74d | |||
| b3ec40ae65 | |||
| 87f0cb016d | |||
| 381e67bb39 | |||
| c2f4de822f | |||
| ffd4c03d9c | |||
| 2e08ac06ca | |||
| 6d739972de | |||
| e36fd43f9b | |||
| 35bbc6c61b | |||
| 987c704730 | |||
| 9669f2aa0b | |||
| 99139e360d | |||
| 696a92096d | |||
| f125c23fb9 | |||
| c50d6c4bfe | |||
| 5edea74333 | |||
| 1882a94e65 | |||
| 1309293705 | 
							
								
								
									
										2
									
								
								.hgignore → .gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,3 @@
 | 
				
			|||||||
syntax: glob
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
cmd/contented/contented
 | 
					cmd/contented/contented
 | 
				
			||||||
build/
 | 
					build/
 | 
				
			||||||
_dist/
 | 
					_dist/
 | 
				
			||||||
							
								
								
									
										2
									
								
								.hgtags
									
									
									
									
									
								
							
							
						
						@@ -1,2 +0,0 @@
 | 
				
			|||||||
e2250a7fd29052ea767f18e1459cabea4cd7efd3 release-1.0.0
 | 
					 | 
				
			||||||
b8975b9e75648a7c2a5003c67db92cf2216e01c0 release-1.0.1
 | 
					 | 
				
			||||||
							
								
								
									
										32
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						@@ -2,7 +2,7 @@
 | 
				
			|||||||
# Makefile for contented
 | 
					# Makefile for contented
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
VERSION:=1.1.0
 | 
					VERSION:=1.2.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SOURCES:=Makefile \
 | 
					SOURCES:=Makefile \
 | 
				
			||||||
	static \
 | 
						static \
 | 
				
			||||||
@@ -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)
 | 
					 | 
				
			||||||
	 hg archive --type=zip _dist/contented-$(VERSION)-src.zip
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								Metadata.go
									
									
									
									
									
								
							
							
						
						@@ -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 (
 | 
				
			||||||
@@ -47,7 +48,7 @@ func idToString(v uint64) string {
 | 
				
			|||||||
	hd := hashids.NewData()
 | 
						hd := hashids.NewData()
 | 
				
			||||||
	hd.Salt = hashIdSalt
 | 
						hd.Salt = hashIdSalt
 | 
				
			||||||
	hd.MinLength = hashIdMinLength
 | 
						hd.MinLength = hashIdMinLength
 | 
				
			||||||
	h, _ := hashids.NewWithData(hd)
 | 
						h := hashids.NewWithData(hd)
 | 
				
			||||||
	s, _ := h.EncodeInt64([]int64{int64(v)})
 | 
						s, _ := h.EncodeInt64([]int64{int64(v)})
 | 
				
			||||||
	return s
 | 
						return s
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										151
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,151 @@
 | 
				
			|||||||
 | 
					# contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[](doc/image1.png)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					- Multiple files upload
 | 
				
			||||||
 | 
					- 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
 | 
				
			||||||
 | 
					- Hash verification (SHA512/256)
 | 
				
			||||||
 | 
					- Detect duplicate upload content and reuse storage
 | 
				
			||||||
 | 
					- Options to limit the upload filesize and the upload bandwidth
 | 
				
			||||||
 | 
					- Short URLs (using [Hashids](http://hashids.org) algorithm)
 | 
				
			||||||
 | 
					- Image thumbnailing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage (Server)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					Usage of contented:
 | 
				
			||||||
 | 
					  -concurrentthumbs int
 | 
				
			||||||
 | 
					        Simultaneous thumbnail generation (default 16)
 | 
				
			||||||
 | 
					  -data string
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        Maximum size of uploaded files in MiB (set zero for unlimited) (default 8)
 | 
				
			||||||
 | 
					  -speed int
 | 
				
			||||||
 | 
					        Maximum upload speed in bytes/sec (set zero for unlimited)
 | 
				
			||||||
 | 
					  -title string
 | 
				
			||||||
 | 
					        Title used in web interface (default "contented")
 | 
				
			||||||
 | 
					  -trustXForwardedFor
 | 
				
			||||||
 | 
					        Trust X-Forwarded-For reverse proxy headers
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage (HTTP)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 (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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```html
 | 
				
			||||||
 | 
					<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2020-07-25: 1.3.0
 | 
				
			||||||
 | 
					- Feature: Option to limit concurrent thumbnail generation
 | 
				
			||||||
 | 
					- Enhancement: Set charset=UTF-8 when serving user-submitted text/plain content
 | 
				
			||||||
 | 
					- Fix an issue with large memory usage for multipart file uploads
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2018-06-09: 1.2.1
 | 
				
			||||||
 | 
					- Feature: Add OpenGraph tags on preview pages, for rich metadata in chat applications
 | 
				
			||||||
 | 
					- Update thumbnailing library to improve quality
 | 
				
			||||||
 | 
					- Use dep for vendoring
 | 
				
			||||||
 | 
					- [⬇️ contented-1.2.1-win32.7z](https://git.ivysaur.me/attachments/88dea4f7-e314-4325-a957-096dcf8cdecc) *(1.51 MiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.2.1-src.zip](https://git.ivysaur.me/attachments/6fd2b963-3be4-48a6-a5bf-6f273bcaea24) *(1.49 MiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.2.1-linux64.tar.gz](https://git.ivysaur.me/attachments/c536f764-0250-4d67-886a-4797946e1124) *(2.21 MiB)*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-11-18: 1.2.0
 | 
				
			||||||
 | 
					- Feature: Thumbnail support
 | 
				
			||||||
 | 
					- Feature: File preview page
 | 
				
			||||||
 | 
					- Feature: Album mode (via URL `/p/{file1}-{file2}-...`)
 | 
				
			||||||
 | 
					- Feature: New `-diskFilesWorldReadable` option to save files with `0644` mode
 | 
				
			||||||
 | 
					- [⬇️ contented-1.2.0-win32.7z](https://git.ivysaur.me/attachments/f3453b62-b2a7-4e77-9b04-44c99dec35ba) *(1.36 MiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.2.0-src.zip](https://git.ivysaur.me/attachments/a6c1ecfb-fd6a-44b5-9dc8-aea7c439d1e6) *(178.94 KiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.2.0-linux64.tar.gz](https://git.ivysaur.me/attachments/6234754b-af17-4a72-8b66-56a5db21c7c7) *(2.03 MiB)*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-15: 1.1.0
 | 
				
			||||||
 | 
					- Feature: Drawing mode
 | 
				
			||||||
 | 
					- Feature: Ctrl+V image upload
 | 
				
			||||||
 | 
					- Feature: Option to trust X-Forwarded-For headers when using a reverse proxy
 | 
				
			||||||
 | 
					- Feature: Add `getDownloadURL`, `getInfoJSONURL`, `getPreviewURL` SDK methods
 | 
				
			||||||
 | 
					- Feature: Option to disable uploading via the homepage
 | 
				
			||||||
 | 
					- Feature: Add button to repeat when uploading from homepage
 | 
				
			||||||
 | 
					- Enhancement: Automatically load library dependencies
 | 
				
			||||||
 | 
					- Enhancement: Display homepage widget using the full screen size
 | 
				
			||||||
 | 
					- Include drawingboard.js 0.4.6 (MIT license)
 | 
				
			||||||
 | 
					- Fix a cosmetic issue with javascript console output
 | 
				
			||||||
 | 
					- Fix a cosmetic issue with error messages if an upload failed
 | 
				
			||||||
 | 
					- [⬇️ contented-1.1.0-win32.7z](https://git.ivysaur.me/attachments/bfb0a7fe-bf95-4d0e-933b-8137bc8071a4) *(1.11 MiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.1.0-src.zip](https://git.ivysaur.me/attachments/67401341-724f-4ea2-b9c7-44d08ab9d38a) *(142.82 KiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.1.0-linux64.tar.gz](https://git.ivysaur.me/attachments/a13752dd-5228-4830-b61d-0f7cc568b2ae) *(1.67 MiB)*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-08: 1.0.1
 | 
				
			||||||
 | 
					- Fix an issue with CORS preflight requests
 | 
				
			||||||
 | 
					- Fix an issue with index URLs
 | 
				
			||||||
 | 
					- [⬇️ contented-1.0.1-win32.7z](https://git.ivysaur.me/attachments/a873d510-da09-4797-95e9-ffcad690a77b) *(1.10 MiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.0.1-src.zip](https://git.ivysaur.me/attachments/43ac17d6-b6f1-4da7-98e9-b8af6fb5551a) *(109.08 KiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.0.1-linux64.tar.gz](https://git.ivysaur.me/attachments/34d74bed-db3f-4cef-a76f-266f0b9e6017) *(1.65 MiB)*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2017-10-08: 1.0.0
 | 
				
			||||||
 | 
					- Initial public release
 | 
				
			||||||
 | 
					- Include jQuery 1.12.4 (MIT license)
 | 
				
			||||||
 | 
					- [⬇️ contented-1.0.0-win32.7z](https://git.ivysaur.me/attachments/4ef132cf-dac8-4bcf-9da7-14ca1366e815) *(1.10 MiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.0.0-src.zip](https://git.ivysaur.me/attachments/74d77b3f-557b-44bf-9645-7b3b25ab17c1) *(102.45 KiB)*
 | 
				
			||||||
 | 
					- [⬇️ contented-1.0.0-linux64.tar.gz](https://git.ivysaur.me/attachments/1c28a913-686b-44cf-b63d-db22968a93b6) *(1.65 MiB)*
 | 
				
			||||||
							
								
								
									
										65
									
								
								Server.go
									
									
									
									
									
								
							
							
						
						@@ -1,38 +1,62 @@
 | 
				
			|||||||
package contented
 | 
					package contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"bytes"
 | 
						"embed"
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"io/fs"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"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 ALBUM_MIMETYPE = `contented/album`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ServerPublicProperties struct {
 | 
					type ServerPublicProperties struct {
 | 
				
			||||||
	AppTitle         string
 | 
						AppTitle         string
 | 
				
			||||||
	MaxUploadBytes   int64
 | 
						MaxUploadBytes   int64
 | 
				
			||||||
 | 
						CanonicalBaseURL string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type ServerOptions struct {
 | 
					type ServerOptions struct {
 | 
				
			||||||
	DataDirectory          string
 | 
						DataDirectory          string
 | 
				
			||||||
	DBPath                 string
 | 
						DBPath                 string
 | 
				
			||||||
 | 
						DiskFilesWorldReadable bool
 | 
				
			||||||
	BandwidthLimit         int64
 | 
						BandwidthLimit         int64
 | 
				
			||||||
	TrustXForwardedFor     bool
 | 
						TrustXForwardedFor     bool
 | 
				
			||||||
	EnableHomepage         bool
 | 
						EnableHomepage         bool
 | 
				
			||||||
 | 
						EnableUpload           bool
 | 
				
			||||||
 | 
						MaxConcurrentThumbs    int
 | 
				
			||||||
	ServerPublicProperties
 | 
						ServerPublicProperties
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *ServerOptions) FileMode() os.FileMode {
 | 
				
			||||||
 | 
						if this.DiskFilesWorldReadable {
 | 
				
			||||||
 | 
							return 0644
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return 0600
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Server struct {
 | 
					type Server struct {
 | 
				
			||||||
	opts           ServerOptions
 | 
						opts           ServerOptions
 | 
				
			||||||
	db             *bolt.DB
 | 
						db             *bolt.DB
 | 
				
			||||||
	startTime      time.Time
 | 
						startTime      time.Time
 | 
				
			||||||
 | 
						thumbnailSem   chan struct{}
 | 
				
			||||||
	metadataBucket []byte
 | 
						metadataBucket []byte
 | 
				
			||||||
 | 
						staticDir      fs.FS // interface
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewServer(opts *ServerOptions) (*Server, error) {
 | 
					func NewServer(opts *ServerOptions) (*Server, error) {
 | 
				
			||||||
@@ -42,6 +66,19 @@ func NewServer(opts *ServerOptions) (*Server, error) {
 | 
				
			|||||||
		startTime:      time.Now(),
 | 
							startTime:      time.Now(),
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if s.opts.MaxConcurrentThumbs <= 0 {
 | 
				
			||||||
 | 
							s.opts.MaxConcurrentThumbs = DEFAULT_MAX_CONCURRENT_THUMBS // default
 | 
				
			||||||
 | 
							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 {
 | 
				
			||||||
 | 
							s.thumbnailSem <- struct{}{}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	b, err := bolt.Open(opts.DBPath, 0644, bolt.DefaultOptions)
 | 
						b, err := bolt.Open(opts.DBPath, 0644, bolt.DefaultOptions)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
@@ -90,8 +127,11 @@ func (this *Server) remoteIP(r *http.Request) string {
 | 
				
			|||||||
const (
 | 
					const (
 | 
				
			||||||
	downloadUrlPrefix = `/get/`
 | 
						downloadUrlPrefix = `/get/`
 | 
				
			||||||
	metadataUrlPrefix = `/info/`
 | 
						metadataUrlPrefix = `/info/`
 | 
				
			||||||
 | 
						previewUrlPrefix  = `/p/`
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					var rxThumbUrl = regexp.MustCompile(`^/thumb/(.)/(.*)$`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
					func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
	w.Header().Set(`Server`, SERVER_HEADER)
 | 
						w.Header().Set(`Server`, SERVER_HEADER)
 | 
				
			||||||
	w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS
 | 
						w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS
 | 
				
			||||||
@@ -110,6 +150,13 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	} else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, metadataUrlPrefix) {
 | 
						} else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, metadataUrlPrefix) {
 | 
				
			||||||
		this.handleInformation(w, r.URL.Path[len(metadataUrlPrefix):])
 | 
							this.handleInformation(w, r.URL.Path[len(metadataUrlPrefix):])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, previewUrlPrefix) {
 | 
				
			||||||
 | 
							this.handlePreview(w, r.URL.Path[len(previewUrlPrefix):])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						} else if r.Method == "GET" && rxThumbUrl.MatchString(r.URL.Path) {
 | 
				
			||||||
 | 
							parts := rxThumbUrl.FindStringSubmatch(r.URL.Path)
 | 
				
			||||||
 | 
							this.handleThumb(w, r, parts[1][0], parts[2])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	} else if r.Method == "GET" && r.URL.Path == `/about` {
 | 
						} else if r.Method == "GET" && r.URL.Path == `/about` {
 | 
				
			||||||
		this.handleAbout(w)
 | 
							this.handleAbout(w)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -120,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)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								TODO.txt
									
									
									
									
									
								
							
							
						
						@@ -1,13 +0,0 @@
 | 
				
			|||||||
TODO
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- View server-wide recent uploads history / all upload history
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Display 'my uploads' (id + metadata history kept in localstorage)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Encrypted at rest (anti- provider snooping)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Nicer preview page after uploading
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Gallery preview (multiple element hashid)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Prevent selecting around the toggle buttons (firefox for android)
 | 
					 | 
				
			||||||
@@ -1,82 +0,0 @@
 | 
				
			|||||||
A file / image / paste upload server with a focus on embedding.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Written in Go
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
You can use contented as a standalone upload server, or you can use the SDK to embed its upload widget into another website.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
=FEATURES=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Drag and drop upload
 | 
					 | 
				
			||||||
- Multiple files upload
 | 
					 | 
				
			||||||
- Pastebin upload
 | 
					 | 
				
			||||||
- Custom drawing upload ([url=https://github.com/Leimi/drawingboard.js]via[/url])
 | 
					 | 
				
			||||||
- Ctrl-V upload
 | 
					 | 
				
			||||||
- SDK-oriented design for embedding, including CORS support
 | 
					 | 
				
			||||||
- Mobile friendly HTML interface
 | 
					 | 
				
			||||||
- Preserves uploaded filename and content-type metadata
 | 
					 | 
				
			||||||
- Hash verification (SHA512/256)
 | 
					 | 
				
			||||||
- Detect duplicate upload content and reuse storage
 | 
					 | 
				
			||||||
- Options to limit the upload filesize and the upload bandwidth
 | 
					 | 
				
			||||||
- Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
=USAGE (SERVER)=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`Usage of contented:
 | 
					 | 
				
			||||||
  -data string
 | 
					 | 
				
			||||||
        Directory for stored content (default "")
 | 
					 | 
				
			||||||
  -db string
 | 
					 | 
				
			||||||
        Path for metadata database (default "contented.db")
 | 
					 | 
				
			||||||
  -enableHomepage
 | 
					 | 
				
			||||||
        Enable homepage (disable for embedded use only) (default true)
 | 
					 | 
				
			||||||
  -listen string
 | 
					 | 
				
			||||||
         (default "127.0.0.1:80")
 | 
					 | 
				
			||||||
  -max int
 | 
					 | 
				
			||||||
        Maximum size of uploaded files in MiB (set zero for unlimited) (default 8)
 | 
					 | 
				
			||||||
  -speed int
 | 
					 | 
				
			||||||
        Maximum upload speed in bytes/sec (set zero for unlimited)
 | 
					 | 
				
			||||||
  -title string
 | 
					 | 
				
			||||||
        Title used in web interface (default "contented")
 | 
					 | 
				
			||||||
  -trustXForwardedFor
 | 
					 | 
				
			||||||
        Trust X-Forwarded-For reverse proxy headers
 | 
					 | 
				
			||||||
`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
=USAGE (HTTP)=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The server responds on the following URLs:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- `/get/{ID}`: Download item content
 | 
					 | 
				
			||||||
- `/info/{ID}`: Get item content metadata (JSON)
 | 
					 | 
				
			||||||
- `/about`: 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.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
 | 
					 | 
				
			||||||
contented.init("#target", function(/* String[] */ items) {});
 | 
					 | 
				
			||||||
`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
=CHANGELOG=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2017-10-15: 1.1.0
 | 
					 | 
				
			||||||
- Feature: Drawing mode
 | 
					 | 
				
			||||||
- Feature: Ctrl+V image upload
 | 
					 | 
				
			||||||
- Feature: Option to trust X-Forwarded-For headers when using a reverse proxy
 | 
					 | 
				
			||||||
- Feature: Add `getDownloadURL`, `getInfoJSONURL`, `getPreviewURL` SDK methods
 | 
					 | 
				
			||||||
- Feature: Option to disable uploading via the homepage
 | 
					 | 
				
			||||||
- Feature: Add button to repeat when uploading from homepage
 | 
					 | 
				
			||||||
- Enhancement: Automatically load library dependencies
 | 
					 | 
				
			||||||
- Enhancement: Display homepage widget using the full screen size
 | 
					 | 
				
			||||||
- Include drawingboard.js 0.4.6 (MIT license)
 | 
					 | 
				
			||||||
- Fix a cosmetic issue with javascript console output
 | 
					 | 
				
			||||||
- Fix a cosmetic issue with error messages if an upload failed
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2017-10-08: 1.0.1
 | 
					 | 
				
			||||||
- Fix an issue with CORS preflight requests
 | 
					 | 
				
			||||||
- Fix an issue with index URLs
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
2017-10-08: 1.0.0
 | 
					 | 
				
			||||||
- Initial public release
 | 
					 | 
				
			||||||
- Include jQuery 1.12.4 (MIT license)
 | 
					 | 
				
			||||||
@@ -20,6 +20,9 @@ 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")
 | 
				
			||||||
 | 
						maxConcurrentThumbs := flag.Int("concurrentthumbs", contented.DEFAULT_MAX_CONCURRENT_THUMBS, "Simultaneous thumbnail generation")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	flag.Parse()
 | 
						flag.Parse()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -29,6 +32,9 @@ func main() {
 | 
				
			|||||||
		BandwidthLimit:         int64(*maxUploadSpeed),
 | 
							BandwidthLimit:         int64(*maxUploadSpeed),
 | 
				
			||||||
		TrustXForwardedFor:     *trustXForwardedFor,
 | 
							TrustXForwardedFor:     *trustXForwardedFor,
 | 
				
			||||||
		EnableHomepage:         *enableHomepage,
 | 
							EnableHomepage:         *enableHomepage,
 | 
				
			||||||
 | 
							EnableUpload:           *enableUpload,
 | 
				
			||||||
 | 
							DiskFilesWorldReadable: *diskFilesWorldReadable,
 | 
				
			||||||
 | 
							MaxConcurrentThumbs:    *maxConcurrentThumbs,
 | 
				
			||||||
		ServerPublicProperties: contented.ServerPublicProperties{
 | 
							ServerPublicProperties: contented.ServerPublicProperties{
 | 
				
			||||||
			AppTitle:       *appTitle,
 | 
								AppTitle:       *appTitle,
 | 
				
			||||||
			MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024,
 | 
								MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024,
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								doc/image1.thumb.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.3 KiB  | 
							
								
								
									
										10
									
								
								download.go
									
									
									
									
									
								
							
							
						
						@@ -37,9 +37,17 @@ func (this *Server) handleViewInternal(w http.ResponseWriter, r *http.Request, f
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// ServeContent only uses the filename to get the mime type, which we can
 | 
						// ServeContent only uses the filename to get the mime type, which we can
 | 
				
			||||||
	// set accurately (including blacklist)
 | 
						// set accurately (including blacklist)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch m.MimeType {
 | 
				
			||||||
 | 
						case `text/plain`:
 | 
				
			||||||
 | 
							w.Header().Set(`Content-Type`, `text/plain; charset=UTF-8`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case `application/octet-stream`:
 | 
				
			||||||
		w.Header().Set(`Content-Type`, m.MimeType)
 | 
							w.Header().Set(`Content-Type`, m.MimeType)
 | 
				
			||||||
	if m.MimeType == `application/octet-stream` {
 | 
					 | 
				
			||||||
		w.Header().Set(`Content-Disposition`, `attachment; filename="`+m.Filename+`"`)
 | 
							w.Header().Set(`Content-Disposition`, `attachment; filename="`+m.Filename+`"`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							w.Header().Set(`Content-Type`, m.MimeType)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	http.ServeContent(w, r, "", m.UploadTime, f)
 | 
						http.ServeContent(w, r, "", m.UploadTime, f)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										17
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					module code.ivysaur.me/contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						code.ivysaur.me/thumbnail v1.0.2
 | 
				
			||||||
 | 
						github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
 | 
				
			||||||
 | 
						github.com/speps/go-hashids v1.0.0
 | 
				
			||||||
 | 
						go.etcd.io/bbolt v1.3.7
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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
 | 
				
			||||||
							
								
								
									
										21
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					code.ivysaur.me/imagequant/v2 v2.12.6 h1:xYrGj6GOdAcutmzqBxG7bDZ70r4jYHADOCZ+ktyMU3Y=
 | 
				
			||||||
 | 
					code.ivysaur.me/imagequant/v2 v2.12.6/go.mod h1:seCAm0sP2IBsb1YNBj4D+EZovIuGe16+6Xo0aiGyhDU=
 | 
				
			||||||
 | 
					code.ivysaur.me/thumbnail v1.0.2 h1:vQaRPbBZOUGpr4b5rrUOHiZv08XSRJ83uu64WXFx7mo=
 | 
				
			||||||
 | 
					code.ivysaur.me/thumbnail v1.0.2/go.mod h1:sXeHBfmPfiSe5ZBKsbGSES13C9OSZq0WmT4yZ/XBeeE=
 | 
				
			||||||
 | 
					github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
 | 
				
			||||||
 | 
					github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
 | 
				
			||||||
 | 
					github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 | 
				
			||||||
 | 
					github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
 | 
				
			||||||
 | 
					github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
 | 
				
			||||||
 | 
					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=
 | 
				
			||||||
							
								
								
									
										196
									
								
								preview.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,196 @@
 | 
				
			|||||||
 | 
					package contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"encoding/json"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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(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) + `" />
 | 
				
			||||||
 | 
							<meta property="og:type" content="website" />
 | 
				
			||||||
 | 
					`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if len(this.opts.ServerPublicProperties.CanonicalBaseURL) > 0 {
 | 
				
			||||||
 | 
							tmpl += `
 | 
				
			||||||
 | 
							<meta property="og:url" content="` + html.EscapeString(this.opts.ServerPublicProperties.CanonicalBaseURL+`p/`+fileIDList) + `" />
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, fileID := range fileIDs {
 | 
				
			||||||
 | 
							tmpl += `
 | 
				
			||||||
 | 
							<meta property="og:image" content="` + html.EscapeString(`/thumb/m/`+fileID) + `" />
 | 
				
			||||||
 | 
							<meta property="og:image:type" content="image/jpeg" />
 | 
				
			||||||
 | 
							<meta property="og:image:width" content="300" />
 | 
				
			||||||
 | 
							<meta property="og:image:height" content="300" />
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tmpl += `
 | 
				
			||||||
 | 
							<style type="text/css">
 | 
				
			||||||
 | 
					html, body {
 | 
				
			||||||
 | 
						background: #333;
 | 
				
			||||||
 | 
						color: #F0F0F0;
 | 
				
			||||||
 | 
						font-family: sans-serif;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.entry {
 | 
				
			||||||
 | 
						display: inline-block;
 | 
				
			||||||
 | 
						margin: 4px;
 | 
				
			||||||
 | 
						border-radius: 4px;
 | 
				
			||||||
 | 
						max-width: 340px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.thumbnail {
 | 
				
			||||||
 | 
						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;
 | 
				
			||||||
 | 
						padding: 4px;
 | 
				
			||||||
 | 
						word-break: break-word;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							</style>
 | 
				
			||||||
 | 
						</head>
 | 
				
			||||||
 | 
						<body>
 | 
				
			||||||
 | 
							<div class="container">
 | 
				
			||||||
 | 
						`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, fileID := range fileIDs {
 | 
				
			||||||
 | 
							m, err := this.Metadata(fileID)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								if os.IsNotExist(err) {
 | 
				
			||||||
 | 
									http.Error(w, "Not found", 404)
 | 
				
			||||||
 | 
									return
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								log.Println(err.Error())
 | 
				
			||||||
 | 
								http.Error(w, "Internal error", 500)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							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>
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							} 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>
 | 
				
			||||||
 | 
								`
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if this.opts.EnableHomepage {
 | 
				
			||||||
 | 
							tmpl += `
 | 
				
			||||||
 | 
								<div class="return">
 | 
				
			||||||
 | 
									<button onclick="window.location.href='/'">Again...</button>
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						tmpl += `
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</body>
 | 
				
			||||||
 | 
					</html>`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set(`Content-Type`, `text/html; charset=UTF-8`)
 | 
				
			||||||
 | 
						w.Header().Set(`Content-Length`, fmt.Sprintf("%d", len(tmpl)))
 | 
				
			||||||
 | 
						w.WriteHeader(200)
 | 
				
			||||||
 | 
						w.Write([]byte(tmpl))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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";
 | 
				
			||||||
@@ -56,22 +56,7 @@ $.get("/about", function(ret) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Load upload widget
 | 
					// Load upload widget
 | 
				
			||||||
contented.init("#surrogate-area", function(items) {
 | 
					contented.init("#surrogate-area", function(items) {
 | 
				
			||||||
    
 | 
					    window.location.href = contented.getMultiPreviewURL(items);
 | 
				
			||||||
    var $table = $("<table>");
 | 
					 | 
				
			||||||
    for (var i = 0; i < items.length; ++i) {
 | 
					 | 
				
			||||||
        $table.append($("<tr>").append([
 | 
					 | 
				
			||||||
            $("<td>").text(items[i]),
 | 
					 | 
				
			||||||
            $("<td>").html("<a target='_blank' href='" + contented.getDownloadURL(items[i]) + "'>get</a>"),
 | 
					 | 
				
			||||||
            $("<td>").html("<a target='_blank' href='" + contented.getInfoJSONURL(items[i]) + "'>info</a>"),
 | 
					 | 
				
			||||||
        ]))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $("#surrogate-area").html([
 | 
					 | 
				
			||||||
		$table,
 | 
					 | 
				
			||||||
		$("<button>").addClass("again").text("Again...").click(function() {
 | 
					 | 
				
			||||||
			window.location.href = window.location.href;
 | 
					 | 
				
			||||||
		}),
 | 
					 | 
				
			||||||
	]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
		</script>
 | 
							</script>
 | 
				
			||||||
	</body>
 | 
						</body>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								static/jquery-1.12.4.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
							
								
								
									
										2
									
								
								static/jquery-3.7.0.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/nothumb_1024.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 8.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/nothumb_160.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.8 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/nothumb_340.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/nothumb_640.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 5.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								static/nothumb_90.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										420
									
								
								static/sdk.js
									
									
									
									
									
								
							
							
						
						@@ -1,28 +1,6 @@
 | 
				
			|||||||
;
 | 
					;(function() {
 | 
				
			||||||
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
var contented = (function() {
 | 
					 | 
				
			||||||
	"use strict";
 | 
						"use strict";
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	// @ref https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob 
 | 
					 | 
				
			||||||
	if (!HTMLCanvasElement.prototype.toBlob) {
 | 
					 | 
				
			||||||
		Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
 | 
					 | 
				
			||||||
			value: function (callback, type, quality) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
 | 
					 | 
				
			||||||
					len = binStr.length,
 | 
					 | 
				
			||||||
					arr = new Uint8Array(len);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				for (var i = 0; i < len; i++ ) {
 | 
					 | 
				
			||||||
					arr[i] = binStr.charCodeAt(i);
 | 
					 | 
				
			||||||
				}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
				callback( new Blob( [arr], {type: type || 'image/png'} ) );
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	var getCurrentScriptPath = function () {
 | 
						var getCurrentScriptPath = function () {
 | 
				
			||||||
		// Determine current script path
 | 
							// Determine current script path
 | 
				
			||||||
		// @ref https://stackoverflow.com/a/26023176
 | 
							// @ref https://stackoverflow.com/a/26023176
 | 
				
			||||||
@@ -34,75 +12,14 @@ var contented = (function() {
 | 
				
			|||||||
		return currentScript.replace(currentScriptFile, '');
 | 
							return currentScript.replace(currentScriptFile, '');
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	var currentScriptPath = getCurrentScriptPath();
 | 
						var loadScript = function(url) {
 | 
				
			||||||
	var baseURL = currentScriptPath.replace('sdk.js', '');
 | 
							return new Promise(function(resolve, reject) {
 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	return {
 | 
					 | 
				
			||||||
		"loaded": false,
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		"baseURL": baseURL,
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		"__preInit": [],
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		/**
 | 
					 | 
				
			||||||
		* initArea shows the contented upload widget over the top of a target DOM element.
 | 
					 | 
				
			||||||
		*
 | 
					 | 
				
			||||||
		* @param any        element       Drop target (string selector / DOMElement / jQuery)
 | 
					 | 
				
			||||||
		* @param Function   onUploaded    Called with an array of upload IDs
 | 
					 | 
				
			||||||
		* @param Function   onClose       Called when the widget is being destroyed
 | 
					 | 
				
			||||||
		*/
 | 
					 | 
				
			||||||
		"init": function(elementSelector, onUploaded, onClose) {
 | 
					 | 
				
			||||||
			contented.__preInit.push([elementSelector, onUploaded, onClose]);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		/**
 | 
					 | 
				
			||||||
		* supportsDrop returns whether drag-and-drop is supported by this browser.
 | 
					 | 
				
			||||||
		*
 | 
					 | 
				
			||||||
		* @return bool
 | 
					 | 
				
			||||||
		*/
 | 
					 | 
				
			||||||
		"supportsDrop": function() {
 | 
					 | 
				
			||||||
			return ('ondrop' in window && 'FormData' in window && 'FileReader' in window);
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		"getPreviewURL": function(id) {
 | 
					 | 
				
			||||||
			return baseURL + "get/" + id; // n.b. there's no better preview URL yet
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"getDownloadURL": function(id) {
 | 
					 | 
				
			||||||
			return baseURL + "get/" + id;
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		"getInfoJSONURL": function(id) {
 | 
					 | 
				
			||||||
			return baseURL + "info/" + id;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
})();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
;(function() {
 | 
					 | 
				
			||||||
	"use strict";
 | 
					 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	var loadScript = function(url, onLoad) {
 | 
					 | 
				
			||||||
			var script = document.createElement('script');
 | 
								var script = document.createElement('script');
 | 
				
			||||||
		script.onload = onLoad;
 | 
								script.onload = resolve;
 | 
				
			||||||
 | 
								script.onerror = reject;
 | 
				
			||||||
			script.src = url;
 | 
								script.src = url;
 | 
				
			||||||
			document.head.appendChild(script);
 | 
								document.head.appendChild(script);
 | 
				
			||||||
	};
 | 
							});
 | 
				
			||||||
	
 | 
					 | 
				
			||||||
	var loadScripts = function(urls, onLoad) {
 | 
					 | 
				
			||||||
		if (urls.length === 0) {
 | 
					 | 
				
			||||||
			onLoad();
 | 
					 | 
				
			||||||
			return;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		
 | 
					 | 
				
			||||||
		var totalLoaded = 0;
 | 
					 | 
				
			||||||
		var cb = function() {
 | 
					 | 
				
			||||||
			totalLoaded += 1;
 | 
					 | 
				
			||||||
			if (totalLoaded == urls.length) {
 | 
					 | 
				
			||||||
				onLoad();
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
		for (var i = 0; i < urls.length; ++i) {
 | 
					 | 
				
			||||||
			loadScript(urls[i], cb);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
    var formatBytes = function(bytes) {
 | 
					    var formatBytes = function(bytes) {
 | 
				
			||||||
@@ -127,9 +44,184 @@ var contented = (function() {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var afterScriptsLoaded = function() {		
 | 
						var widgetHtml = `
 | 
				
			||||||
 | 
					<style type="text/css">
 | 
				
			||||||
 | 
						.contented {
 | 
				
			||||||
 | 
							box-sizing:border-box;
 | 
				
			||||||
 | 
							text-align: center;
 | 
				
			||||||
 | 
							border: 8px dashed lightgrey;
 | 
				
			||||||
 | 
							padding: 12px;
 | 
				
			||||||
 | 
							background:white; /* not transparent */
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
		var initArea = function (elementSelector, onUploaded, onClose) {
 | 
							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 class="contented-upload-type" data-upload-type="album" title="Album">
 | 
				
			||||||
 | 
								<svg viewBox="0 0 24 24">
 | 
				
			||||||
 | 
									<path fill="#000000" d="M3,3H21V7H3V3M4,8H20V21H4V8M9.5,11A0.5,0.5 0 0,0 9,11.5V13H15V11.5A0.5,0.5 0 0,0 14.5,11H9.5Z" />
 | 
				
			||||||
 | 
								</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 class="contented-upload-if contented-if-album">
 | 
				
			||||||
 | 
								<input type="text" class="contented-album-title" placeholder="Album name"><br>
 | 
				
			||||||
 | 
								<input type="text" class="contented-album-items" placeholder="Image IDs, separated with commas">
 | 
				
			||||||
 | 
								<br><br>
 | 
				
			||||||
 | 
								<button class="contented-album-upload">Make album »</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var initArea = function (aboutInfo, elementSelector, onUploaded, onClose) {
 | 
				
			||||||
		onUploaded = onUploaded || function () { };
 | 
							onUploaded = onUploaded || function () { };
 | 
				
			||||||
		onClose = onClose || function () { };
 | 
							onClose = onClose || function () { };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -141,15 +233,12 @@ var contented = (function() {
 | 
				
			|||||||
		// <input type="hidden" name="MAX_FILE_SIZE" value="` + ret.MaxUploadBytes + `" /> 
 | 
							// <input type="hidden" name="MAX_FILE_SIZE" value="` + ret.MaxUploadBytes + `" /> 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Create a new div for ourselves on top of the existing area
 | 
							// Create a new div for ourselves on top of the existing area
 | 
				
			||||||
			$.get(contented.baseURL + "about", function (ret) {
 | 
					 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		var extraText = "";
 | 
							var extraText = "";
 | 
				
			||||||
				if (ret.MaxUploadBytes > 0) {
 | 
							if (aboutInfo.MaxUploadBytes > 0) {
 | 
				
			||||||
					extraText = " (max " + formatBytes(ret.MaxUploadBytes) + ")";
 | 
								extraText = " (max " + formatBytes(aboutInfo.MaxUploadBytes) + ")";
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				$.get(contented.baseURL + "widget.html", function (widgetHtml) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		var $f = $("<div>").html(widgetHtml);
 | 
							var $f = $("<div>").html(widgetHtml);
 | 
				
			||||||
		$f.find(".contented-extratext").text(extraText);
 | 
							$f.find(".contented-extratext").text(extraText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -231,6 +320,37 @@ var contented = (function() {
 | 
				
			|||||||
			handleUploadFrom([blob]);
 | 
								handleUploadFrom([blob]);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Album
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							$f.find(".contented-album-upload").on("click", function(e) {
 | 
				
			||||||
 | 
								e.preventDefault();
 | 
				
			||||||
 | 
								e.stopPropagation();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var title = $(".contented-album-title").val();
 | 
				
			||||||
 | 
								if (title === "") {
 | 
				
			||||||
 | 
									title = "Untitled album";
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var childIDs = $(".contented-album-items").val().split(",");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								for (var i = 0; i < childIDs.length; ++i) {
 | 
				
			||||||
 | 
									childIDs[i] = childIDs[i].trim(); // Basic validation - can't really perform full validation here
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (childIDs[i].length == 0) {
 | 
				
			||||||
 | 
										alert("Entry " + (i+1) + " is too short, expected non-zero length");
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									if (! childIDs[i].match(/^[a-zA-Z0-9]+$/)) {
 | 
				
			||||||
 | 
										alert("Entry " + (i+1) + " contains unexpected character");
 | 
				
			||||||
 | 
										return;
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								var blob = new Blob([JSON.stringify(childIDs)], {type : 'contented/album'});
 | 
				
			||||||
 | 
								handleUploadFrom([new File([blob], title, {type: 'contented/album'})]);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Ctrl+V uploads
 | 
							// Ctrl+V uploads
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
		var pasteHandler = function(e) {
 | 
							var pasteHandler = function(e) {
 | 
				
			||||||
@@ -294,6 +414,10 @@ var contented = (function() {
 | 
				
			|||||||
		
 | 
							
 | 
				
			||||||
		var setupDrawingBoard = function() {
 | 
							var setupDrawingBoard = function() {
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
 | 
								$("head").append(
 | 
				
			||||||
 | 
									'<link rel="stylesheet" type="text/css" href="' + contented.baseURL + 'drawingboard-0.4.6.min.css">'
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
								
 | 
				
			||||||
			var db_id = "contented-drawing-area-" + guid();
 | 
								var db_id = "contented-drawing-area-" + guid();
 | 
				
			||||||
			var $db = $("<div>").attr('id', db_id);
 | 
								var $db = $("<div>").attr('id', db_id);
 | 
				
			||||||
			
 | 
								
 | 
				
			||||||
@@ -363,6 +487,10 @@ var contented = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		// Common upload handler
 | 
							// Common upload handler
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							/**
 | 
				
			||||||
 | 
							 * 
 | 
				
			||||||
 | 
							 * @param {File[]|Blob[]} files 
 | 
				
			||||||
 | 
							 */
 | 
				
			||||||
		var handleUploadFrom = function(files) {
 | 
							var handleUploadFrom = function(files) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			setProgressCaption("Uploading, please wait...");
 | 
								setProgressCaption("Uploading, please wait...");
 | 
				
			||||||
@@ -416,33 +544,97 @@ var contented = (function() {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					// .
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				});
 | 
						var init = function() {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			});
 | 
							var currentScriptPath = getCurrentScriptPath();
 | 
				
			||||||
 | 
							var baseURL = currentScriptPath.replace('sdk.js', '');
 | 
				
			||||||
		
 | 
							
 | 
				
			||||||
 | 
							// Kick off background promises
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var loader = new Promise(function(resolve, reject) {
 | 
				
			||||||
 | 
								if (typeof jQuery === "undefined") {
 | 
				
			||||||
 | 
									loadScript(contented.baseURL + "jquery-3.7.0.min.js").then(resolve);
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									resolve();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Update fields in global variable
 | 
							}).then(function() { return new Promise(function(resolve, reject) {
 | 
				
			||||||
		contented.init = initArea;
 | 
								if (typeof DrawingBoard === "undefined") {
 | 
				
			||||||
		contented.loaded = true;
 | 
									loadScript(contented.baseURL + "drawingboard-0.4.6.min.js").then(resolve);
 | 
				
			||||||
 | 
								} else {
 | 
				
			||||||
 | 
									resolve();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// Call initArea for all pre-initialised elements
 | 
							})}).then(function() { return new Promise(function(resolve, reject) {
 | 
				
			||||||
		for (var i = 0; i < contented.__preInit.length; ++i) {
 | 
								$.get(contented.baseURL + "about", function (aboutInfo) {
 | 
				
			||||||
			initArea(contented.__preInit[i][0], contented.__preInit[i][1], contented.__preInit[i][2]);
 | 
									resolve(aboutInfo);
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							})}).then(function(aboutInfo) {
 | 
				
			||||||
 | 
								// Update fields in global variable
 | 
				
			||||||
 | 
								window.contented.loaded = true;
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								return aboutInfo;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							window.contented = {
 | 
				
			||||||
 | 
								"loaded": false,
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								"baseURL": baseURL,
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								"__preInit": [],
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								* initArea shows the contented upload widget over the top of a target DOM element.
 | 
				
			||||||
 | 
								*
 | 
				
			||||||
 | 
								* @param any        element       Drop target (string selector / DOMElement / jQuery)
 | 
				
			||||||
 | 
								* @param Function   onUploaded    Called with an array of upload IDs
 | 
				
			||||||
 | 
								* @param Function   onClose       Called when the widget is being destroyed
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
								"init": function(elementSelector, onUploaded, onClose) {
 | 
				
			||||||
 | 
									loader.then(function(aboutInfo) {
 | 
				
			||||||
 | 
										initArea(aboutInfo, elementSelector, onUploaded, onClose);
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								* supportsDrop returns whether drag-and-drop is supported by this browser.
 | 
				
			||||||
 | 
								*
 | 
				
			||||||
 | 
								* @return bool
 | 
				
			||||||
 | 
								*/
 | 
				
			||||||
 | 
								"supportsDrop": function() {
 | 
				
			||||||
 | 
									return ('ondrop' in window && 'FormData' in window && 'FileReader' in window);
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								"getPreviewURL": function(id) {
 | 
				
			||||||
 | 
									return baseURL + "p/" + encodeURIComponent(id);
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"getMultiPreviewURL": function(items) {
 | 
				
			||||||
 | 
									return baseURL + "p/" + encodeURIComponent(items.join("-"));
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"getDownloadURL": function(id) {
 | 
				
			||||||
 | 
									return baseURL + "get/" + encodeURIComponent(id);
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"getInfoJSONURL": function(id) {
 | 
				
			||||||
 | 
									return baseURL + "info/" + encodeURIComponent(id);
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"getThumbnailURL": function(thumbnailType, id) {
 | 
				
			||||||
 | 
									return baseURL + "thumb/" + encodeURIComponent(thumbnailType) + "/" + encodeURIComponent(id);
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								"thumbnail": {
 | 
				
			||||||
 | 
									"small_square": "s",
 | 
				
			||||||
 | 
									"medium_square": "b",
 | 
				
			||||||
 | 
									"medium": "t",
 | 
				
			||||||
 | 
									"large": "m",
 | 
				
			||||||
 | 
									"xlarge": "l",
 | 
				
			||||||
 | 
									"xxlarge": "h"
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
	// Load scripts
 | 
						};
 | 
				
			||||||
	var needScripts = [];
 | 
					 | 
				
			||||||
	if (typeof jQuery === "undefined") {
 | 
					 | 
				
			||||||
		needScripts.push("jquery-1.12.4.min.js");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (typeof DrawingBoard === "undefined") {
 | 
					 | 
				
			||||||
		needScripts.push("drawingboard-0.4.6.min.js");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	loadScripts(needScripts, afterScriptsLoaded);
 | 
						init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
})()
 | 
					})()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,160 +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;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
.contented-progress-element {
 | 
					 | 
				
			||||||
    position:absolute;
 | 
					 | 
				
			||||||
    background:darkgreen;
 | 
					 | 
				
			||||||
    left:0;
 | 
					 | 
				
			||||||
    width:0%;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
</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">
 | 
					 | 
				
			||||||
			<link rel="stylesheet" type="text/css" href="/drawingboard-0.4.6.min.css">
 | 
					 | 
				
			||||||
			<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>
 | 
					 | 
				
			||||||
							
								
								
									
										109
									
								
								thumb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					package contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"context"
 | 
				
			||||||
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"path/filepath"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"code.ivysaur.me/thumbnail"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func getThumbnailerConfig(t byte) (*thumbnail.Config, error) {
 | 
				
			||||||
 | 
						// Modelled on what imgur.com offers
 | 
				
			||||||
 | 
						// @ref https://api.imgur.com/models/image#thumbs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts := thumbnail.Config{
 | 
				
			||||||
 | 
							Aspect: thumbnail.FitOutside,
 | 
				
			||||||
 | 
							Output: thumbnail.Jpeg,
 | 
				
			||||||
 | 
							Scale:  thumbnail.Bicubic,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch t {
 | 
				
			||||||
 | 
						case 's':
 | 
				
			||||||
 | 
							opts.Width = 90
 | 
				
			||||||
 | 
							opts.Height = 90
 | 
				
			||||||
 | 
						case 'b':
 | 
				
			||||||
 | 
							opts.Width = 160
 | 
				
			||||||
 | 
							opts.Height = 160
 | 
				
			||||||
 | 
						case 't':
 | 
				
			||||||
 | 
							opts.Width = 160
 | 
				
			||||||
 | 
							opts.Height = 160
 | 
				
			||||||
 | 
							// thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY
 | 
				
			||||||
 | 
						case 'm':
 | 
				
			||||||
 | 
							opts.Width = 340
 | 
				
			||||||
 | 
							opts.Height = 340
 | 
				
			||||||
 | 
							// thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY
 | 
				
			||||||
 | 
						case 'l':
 | 
				
			||||||
 | 
							opts.Width = 640
 | 
				
			||||||
 | 
							opts.Height = 640
 | 
				
			||||||
 | 
							// thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY
 | 
				
			||||||
 | 
						case 'h':
 | 
				
			||||||
 | 
							opts.Width = 1024
 | 
				
			||||||
 | 
							opts.Height = 1024
 | 
				
			||||||
 | 
							// thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return nil, errors.New("Unsupported thumbnail type (should be s/b/t/m/l/h)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return &opts, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *Server) handleThumb(w http.ResponseWriter, r *http.Request, thumbnailType byte, fileId string) {
 | 
				
			||||||
 | 
						ctx := r.Context()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						opts, err := getThumbnailerConfig(thumbnailType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error())
 | 
				
			||||||
 | 
							http.Error(w, err.Error(), 400)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Only a limited number of thumbnails can be generated concurrently
 | 
				
			||||||
 | 
						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 {
 | 
				
			||||||
 | 
							// The request was already cancelled
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						t := thumbnail.NewThumbnailerEx(opts)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						err = this.handleThumbInternal(ctx, w, t, fileId)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							w.Header().Set(`Location`, fmt.Sprintf(`/nothumb_%d.png`, opts.Height))
 | 
				
			||||||
 | 
							w.WriteHeader(302)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *Server) handleThumbInternal(ctx context.Context, w http.ResponseWriter, t thumbnail.Thumbnailer, fileId string) error {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Load metadata
 | 
				
			||||||
 | 
						m, err := this.Metadata(fileId)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						filePath := filepath.Join(this.opts.DataDirectory, m.FileHash)
 | 
				
			||||||
 | 
						thumb, err := t.RenderFileAs(filePath, m.MimeType)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						w.Header().Set(`Content-Length`, fmt.Sprintf("%d", len(thumb)))
 | 
				
			||||||
 | 
						w.Header().Set(`Content-Type`, `image/jpeg`)
 | 
				
			||||||
 | 
						w.WriteHeader(200)
 | 
				
			||||||
 | 
						w.Write(thumb)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -18,9 +18,14 @@ 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(this.opts.MaxUploadBytes * 2)
 | 
						err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("%s Invalid request: %s\n", remoteIP, err.Error())
 | 
							log.Printf("%s Invalid request: %s\n", remoteIP, err.Error())
 | 
				
			||||||
		http.Error(w, "Invalid request", 400)
 | 
							http.Error(w, "Invalid request", 400)
 | 
				
			||||||
@@ -91,7 +96,7 @@ func (this *Server) handleUploadFile(src multipart.File, hdr *multipart.FileHead
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Save file to disk
 | 
						// Save file to disk
 | 
				
			||||||
	fileHash := hex.EncodeToString(hasher.Sum(nil))
 | 
						fileHash := hex.EncodeToString(hasher.Sum(nil))
 | 
				
			||||||
	dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, 0600)
 | 
						dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, this.opts.FileMode())
 | 
				
			||||||
	shouldSave := true
 | 
						shouldSave := true
 | 
				
			||||||
	if err != nil && os.IsExist(err) {
 | 
						if err != nil && os.IsExist(err) {
 | 
				
			||||||
		// hash matches existing upload
 | 
							// hash matches existing upload
 | 
				
			||||||
 
 | 
				
			|||||||