Compare commits
	
		
			97 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| d24c3d4895 | |||
| 5e9fcc09c8 | |||
| fb7f1dd3a5 | |||
| 2f4fe0a55f | |||
| a3fc9092e3 | |||
| 6eb48bf72f | |||
| 40aa6c8917 | |||
| 24febefba4 | |||
| 57d9b4d324 | |||
| 3ca73e3221 | |||
| 27cf4cf0c0 | |||
| ef6b680b7e | |||
| 188ca7e679 | |||
| b8f1b26aba | |||
| ac768524ee | |||
| a5008ab455 | |||
| 9436dcd43e | |||
| f543347b1c | |||
| 582854878b | |||
| 642ee5a485 | |||
| 19b20daded | |||
| 270e6a9397 | |||
| ef5351a075 | |||
| 01ac021c7e | 
							
								
								
									
										2
									
								
								.hgignore → .gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,5 +1,3 @@
 | 
				
			|||||||
syntax: glob
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
cmd/contented/contented
 | 
					cmd/contented/contented
 | 
				
			||||||
build/
 | 
					build/
 | 
				
			||||||
_dist/
 | 
					_dist/
 | 
				
			||||||
							
								
								
									
										4
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						@@ -2,7 +2,7 @@
 | 
				
			|||||||
# Makefile for contented
 | 
					# Makefile for contented
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					
 | 
				
			||||||
VERSION:=1.0.1
 | 
					VERSION:=1.2.2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SOURCES:=Makefile \
 | 
					SOURCES:=Makefile \
 | 
				
			||||||
	static \
 | 
						static \
 | 
				
			||||||
@@ -69,5 +69,5 @@ _dist/contented-$(VERSION)-win32.7z: build/win32/contented.exe
 | 
				
			|||||||
	)
 | 
						)
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
_dist/contented-$(VERSION)-src.zip: $(SOURCES)
 | 
					_dist/contented-$(VERSION)-src.zip: $(SOURCES)
 | 
				
			||||||
	 hg archive --type=zip _dist/contented-$(VERSION)-src.zip
 | 
						git archive HEAD -o _dist/contented-$(VERSION)-src.zip
 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										128
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,128 @@
 | 
				
			|||||||
 | 
					# 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## 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
 | 
				
			||||||
 | 
					- 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:
 | 
				
			||||||
 | 
					  -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)
 | 
				
			||||||
 | 
					  -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
 | 
				
			||||||
 | 
					  -concurrentthumbs
 | 
				
			||||||
 | 
					        Simultaneous thumbnail generation (default 16)
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you are hosting behind a reverse proxy, remember to set its post body size parameter appropriately (e.g. `client_max_body_size` for nginx).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage (HTTP)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The server responds on the following URLs:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					URL                  |Method |Description
 | 
				
			||||||
 | 
					---------------------|-------|---
 | 
				
			||||||
 | 
					`/get/{ID}`          |`GET`  |Download item content
 | 
				
			||||||
 | 
					`/info/{ID}`         |`GET`  |Get item content metadata (JSON)
 | 
				
			||||||
 | 
					`/thumb/{Type}/{ID}` |`GET`  |Get item thumbnail image
 | 
				
			||||||
 | 
					`/about`             |`GET`  |Get server metadata (JSON)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Usage (Embedding for web)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element. Your callback will be passed an array of file IDs of any uploaded items.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```html
 | 
				
			||||||
 | 
					<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
 | 
				
			||||||
 | 
					contented.init("#target", function(/* String[] */ items) {});
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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)*
 | 
				
			||||||
							
								
								
									
										61
									
								
								Server.go
									
									
									
									
									
								
							
							
						
						@@ -5,6 +5,8 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"log"
 | 
						"log"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"regexp"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,22 +16,38 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
var SERVER_HEADER string = `contented/0.0.0-dev`
 | 
					var SERVER_HEADER string = `contented/0.0.0-dev`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const DEFAULT_MAX_CONCURRENT_THUMBS = 16
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
	BandwidthLimit int64
 | 
						DiskFilesWorldReadable bool
 | 
				
			||||||
 | 
						BandwidthLimit         int64
 | 
				
			||||||
 | 
						TrustXForwardedFor     bool
 | 
				
			||||||
 | 
						EnableHomepage         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
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,6 +58,17 @@ 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)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// "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
 | 
				
			||||||
@@ -75,15 +104,24 @@ func (this *Server) handleAbout(w http.ResponseWriter) {
 | 
				
			|||||||
	this.serveJsonObject(w, this.opts.ServerPublicProperties)
 | 
						this.serveJsonObject(w, this.opts.ServerPublicProperties)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func remoteIP(r *http.Request) string {
 | 
					func (this *Server) remoteIP(r *http.Request) string {
 | 
				
			||||||
 | 
						if this.opts.TrustXForwardedFor {
 | 
				
			||||||
 | 
							if xff := r.Header.Get("X-Forwarded-For"); len(xff) > 0 {
 | 
				
			||||||
 | 
								return xff
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return strings.TrimRight(strings.TrimRight(r.RemoteAddr, "0123456789"), ":")
 | 
						return strings.TrimRight(strings.TrimRight(r.RemoteAddr, "0123456789"), ":")
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
				
			||||||
@@ -102,6 +140,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)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -112,10 +157,10 @@ 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 == `/` {
 | 
						} else if r.Method == "GET" && r.URL.Path == `/` && this.opts.EnableHomepage {
 | 
				
			||||||
		http.Redirect(w, r, `/index.html`, http.StatusFound)
 | 
							http.Redirect(w, r, `/index.html`, http.StatusFound)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" {
 | 
						} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" && (this.opts.EnableHomepage || r.URL.Path != `/index.html`) {
 | 
				
			||||||
		http.ServeContent(w, r, r.URL.Path[1:], this.startTime, bytes.NewReader(static))
 | 
							http.ServeContent(w, r, r.URL.Path[1:], this.startTime, bytes.NewReader(static))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	} else {
 | 
						} else {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Option to disable manual uploading from the landing page
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
- Gallery preview (multiple element hashid)
 | 
					 | 
				
			||||||
@@ -1,60 +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 uploader, or fallback classic uploader, or pastebin-style uploader
 | 
					 | 
				
			||||||
- Multiple files upload
 | 
					 | 
				
			||||||
- Pastebin 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")
 | 
					 | 
				
			||||||
  -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")
 | 
					 | 
				
			||||||
`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
=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. The SDK depends on jQuery.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
`<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
 | 
					 | 
				
			||||||
contented.init("#target", function(/* String[] */ items) {});
 | 
					 | 
				
			||||||
`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
=CHANGELOG=
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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
 | 
					 | 
				
			||||||
@@ -18,13 +18,21 @@ func main() {
 | 
				
			|||||||
	appTitle := flag.String("title", "contented", "Title used in web interface")
 | 
						appTitle := flag.String("title", "contented", "Title used in web interface")
 | 
				
			||||||
	maxUploadMb := flag.Int("max", 8, "Maximum size of uploaded files in MiB (set zero for unlimited)")
 | 
						maxUploadMb := flag.Int("max", 8, "Maximum size of uploaded files in MiB (set zero for unlimited)")
 | 
				
			||||||
	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")
 | 
				
			||||||
 | 
						enableHomepage := flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)")
 | 
				
			||||||
 | 
						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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	svr, err := contented.NewServer(&contented.ServerOptions{
 | 
						svr, err := contented.NewServer(&contented.ServerOptions{
 | 
				
			||||||
		DataDirectory:  *dataDir,
 | 
							DataDirectory:          *dataDir,
 | 
				
			||||||
		DBPath:         *dbPath,
 | 
							DBPath:                 *dbPath,
 | 
				
			||||||
		BandwidthLimit: int64(*maxUploadSpeed),
 | 
							BandwidthLimit:         int64(*maxUploadSpeed),
 | 
				
			||||||
 | 
							TrustXForwardedFor:     *trustXForwardedFor,
 | 
				
			||||||
 | 
							EnableHomepage:         *enableHomepage,
 | 
				
			||||||
 | 
							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  | 
							
								
								
									
										14
									
								
								download.go
									
									
									
									
									
								
							
							
						
						@@ -10,7 +10,7 @@ import (
 | 
				
			|||||||
func (this *Server) handleView(w http.ResponseWriter, r *http.Request, fileID string) {
 | 
					func (this *Server) handleView(w http.ResponseWriter, r *http.Request, fileID string) {
 | 
				
			||||||
	err := this.handleViewInternal(w, r, r.URL.Path[len(downloadUrlPrefix):])
 | 
						err := this.handleViewInternal(w, r, r.URL.Path[len(downloadUrlPrefix):])
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("%s View failed: %s\n", r.RemoteAddr, err.Error())
 | 
							log.Printf("%s View failed: %s\n", this.remoteIP(r), err.Error())
 | 
				
			||||||
		if os.IsNotExist(err) {
 | 
							if os.IsNotExist(err) {
 | 
				
			||||||
			http.Error(w, "File not found", 404)
 | 
								http.Error(w, "File not found", 404)
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
@@ -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)
 | 
				
			||||||
	w.Header().Set(`Content-Type`, m.MimeType)
 | 
					
 | 
				
			||||||
	if m.MimeType == `application/octet-stream` {
 | 
						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-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)
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										11
									
								
								go.mod
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					module code.ivysaur.me/contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					require (
 | 
				
			||||||
 | 
						code.ivysaur.me/thumbnail v1.0.2
 | 
				
			||||||
 | 
						github.com/boltdb/bolt v1.3.1
 | 
				
			||||||
 | 
						github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
 | 
				
			||||||
 | 
						github.com/speps/go-hashids v1.0.0
 | 
				
			||||||
 | 
						golang.org/x/sys v0.0.0-20180606202747-9527bec2660b // indirect
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					go 1.13
 | 
				
			||||||
							
								
								
									
										17
									
								
								go.sum
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					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=
 | 
				
			||||||
 | 
					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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
				
			||||||
							
								
								
									
										118
									
								
								preview.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					package contented
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import (
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
 | 
						"html"
 | 
				
			||||||
 | 
						"log"
 | 
				
			||||||
 | 
						"net/http"
 | 
				
			||||||
 | 
						"os"
 | 
				
			||||||
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (this *Server) handlePreview(w http.ResponseWriter, fileIDList string) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fileIDs := strings.Split(fileIDList, `-`)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						tmpl := `<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html prefix="og: http://ogp.me/ns#">
 | 
				
			||||||
 | 
						<head>
 | 
				
			||||||
 | 
							<title>` + html.EscapeString(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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					.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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							tmpl += `
 | 
				
			||||||
 | 
								<div class="entry">			
 | 
				
			||||||
 | 
									<div class="thumbnail">
 | 
				
			||||||
 | 
										<a href="` + html.EscapeString(`/get/`+fileID) + `"><img 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))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										5
									
								
								static/drawingboard-0.4.6.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										4
									
								
								static/drawingboard-0.4.6.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -7,9 +7,23 @@
 | 
				
			|||||||
		<style type="text/css">
 | 
							<style type="text/css">
 | 
				
			||||||
html, body {
 | 
					html, body {
 | 
				
			||||||
	font-family: sans-serif;
 | 
						font-family: sans-serif;
 | 
				
			||||||
 | 
						margin:0;
 | 
				
			||||||
 | 
						width:100%;
 | 
				
			||||||
 | 
						height:100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#padder {
 | 
				
			||||||
 | 
						height:100%;
 | 
				
			||||||
 | 
						position:relative;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
#surrogate-area {
 | 
					#surrogate-area {
 | 
				
			||||||
    height:300px;
 | 
					    position:absolute;
 | 
				
			||||||
 | 
					    top:10px;
 | 
				
			||||||
 | 
					    bottom:10px;
 | 
				
			||||||
 | 
					    left:10px;
 | 
				
			||||||
 | 
					    right:10px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					button.again {
 | 
				
			||||||
 | 
						margin-top:2em;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/* hide close button */
 | 
					/* hide close button */
 | 
				
			||||||
.contented-close {
 | 
					.contented-close {
 | 
				
			||||||
@@ -18,9 +32,11 @@ html, body {
 | 
				
			|||||||
		</style>
 | 
							</style>
 | 
				
			||||||
	</head>
 | 
						</head>
 | 
				
			||||||
	<body>
 | 
						<body>
 | 
				
			||||||
        <div id="surrogate-area">
 | 
							<div id="padder">
 | 
				
			||||||
            Loading...
 | 
								<div id="surrogate-area">
 | 
				
			||||||
        </div>
 | 
									Loading...
 | 
				
			||||||
 | 
								</div>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        <script type="text/javascript" src="/jquery-1.12.4.min.js"></script>
 | 
					        <script type="text/javascript" src="/jquery-1.12.4.min.js"></script>
 | 
				
			||||||
		<script type="text/javascript" src="/sdk.js"></script>
 | 
							<script type="text/javascript" src="/sdk.js"></script>
 | 
				
			||||||
@@ -40,17 +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='/get/" + items[i] + "'>get</a>"),
 | 
					 | 
				
			||||||
            $("<td>").html("<a target='_blank' href='/info/" + items[i] + "'>info</a>")
 | 
					 | 
				
			||||||
        ]))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    $("#surrogate-area").html($table);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
		</script>
 | 
							</script>
 | 
				
			||||||
	</body>
 | 
						</body>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										
											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  | 
							
								
								
									
										653
									
								
								static/sdk.js
									
									
									
									
									
								
							
							
						
						@@ -1,214 +1,29 @@
 | 
				
			|||||||
;
 | 
					;
 | 
				
			||||||
var contented = (function ($, currentScriptPath) {
 | 
					 | 
				
			||||||
    "use strict";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var baseURL = currentScriptPath.replace('sdk.js', '');
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var formatBytes = function(bytes) {
 | 
					var contented = (function() {
 | 
				
			||||||
        if (bytes < 1024) {
 | 
						"use strict";
 | 
				
			||||||
            return bytes + " B";
 | 
					 | 
				
			||||||
        } else if (bytes < (1024*1024)) {
 | 
					 | 
				
			||||||
            return (bytes / 1024).toFixed(1) + " KiB";
 | 
					 | 
				
			||||||
        } else if (bytes < (1024*1024*1024)) {
 | 
					 | 
				
			||||||
            return (bytes / (1024*1024)).toFixed(1) + " MiB";
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            return (bytes / (1024*1024*1024)).toFixed(1) + " GiB";
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
	
 | 
						
 | 
				
			||||||
    /**
 | 
						// @ref https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob 
 | 
				
			||||||
     * supportsDrop returns whether drag-and-drop is supported by this browser.
 | 
						if (!HTMLCanvasElement.prototype.toBlob) {
 | 
				
			||||||
     *
 | 
							Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
 | 
				
			||||||
     * @return bool
 | 
								value: function (callback, type, quality) {
 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    var supportsDrop = function () {
 | 
					 | 
				
			||||||
        return ('ondrop' in window && 'FormData' in window && 'FileReader' in window);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
									var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
 | 
				
			||||||
     * initArea shows the contented upload widget over the top of a target DOM element.
 | 
										len = binStr.length,
 | 
				
			||||||
     *
 | 
										arr = new Uint8Array(len);
 | 
				
			||||||
     * @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
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    var initArea = function (elementSelector, onUploaded, onClose) {
 | 
					 | 
				
			||||||
        onUploaded = onUploaded || function () { };
 | 
					 | 
				
			||||||
        onClose = onClose || function () { };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if ($(elementSelector).length != 1) {
 | 
									for (var i = 0; i < len; i++ ) {
 | 
				
			||||||
            return; // should only find one element
 | 
										arr[i] = binStr.charCodeAt(i);
 | 
				
			||||||
        }
 | 
									}
 | 
				
			||||||
        var element = $(elementSelector)[0];
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // <input type="hidden" name="MAX_FILE_SIZE" value="` + ret.MaxUploadBytes + `" /> 
 | 
									callback( new Blob( [arr], {type: type || 'image/png'} ) );
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
        // Create a new div for ourselves on top of the existing area
 | 
							});
 | 
				
			||||||
        $.get(baseURL + "about", function (ret) {
 | 
						}
 | 
				
			||||||
            
 | 
					 | 
				
			||||||
            var extraText = "";
 | 
					 | 
				
			||||||
            if (ret.MaxUploadBytes > 0) {
 | 
					 | 
				
			||||||
                extraText = " (max " + formatBytes(ret.MaxUploadBytes) + ")";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            $.get(baseURL + "widget.html", function (widgetHtml) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var $f = $("<div>").html(widgetHtml);
 | 
					 | 
				
			||||||
                $f.find(".contented-extratext").text(extraText);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var ourClose = function () {
 | 
					 | 
				
			||||||
                    $f.remove(); // remove from dom
 | 
					 | 
				
			||||||
                    onClose(); // upstream close
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                $f.find(".contented-close").click(function () {
 | 
					 | 
				
			||||||
                    ourClose();
 | 
					 | 
				
			||||||
                })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var setType = function (type) {
 | 
					 | 
				
			||||||
                    $f.find(".contented-upload-type").removeClass("contented-upload-type-active");
 | 
					 | 
				
			||||||
                    $f.find(".contented-upload-type[data-upload-type=" + type + "]").addClass("contented-upload-type-active");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    $f.find(".contented-upload-if").removeClass("contented-active");
 | 
					 | 
				
			||||||
                    $f.find(".contented-if-" + type).addClass("contented-active");
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.find(".contented-upload-type").click(function () {
 | 
					 | 
				
			||||||
                    setType($(this).attr('data-upload-type'));
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (!supportsDrop()) {
 | 
					 | 
				
			||||||
                    // switch default
 | 
					 | 
				
			||||||
                    setType('file');
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                //
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var $element = $(element);
 | 
					 | 
				
			||||||
                var offset = $element.offset();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.css({
 | 
					 | 
				
			||||||
                    'position': 'absolute',
 | 
					 | 
				
			||||||
                    'left': offset.left + "px",
 | 
					 | 
				
			||||||
                    'top': offset.top + "px",
 | 
					 | 
				
			||||||
                    'width': $element.width() + "px",
 | 
					 | 
				
			||||||
                    'min-width': $element.width() + "px",
 | 
					 | 
				
			||||||
                    'max-width': $element.width() + "px",
 | 
					 | 
				
			||||||
                    'height': $element.height() + "px",
 | 
					 | 
				
			||||||
                    'min-height': $element.height() + "px",
 | 
					 | 
				
			||||||
                    'max-height': $element.height() + "px"
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.find('.contented').on('dragover dragenter', function (e) {
 | 
					 | 
				
			||||||
                    e.preventDefault();
 | 
					 | 
				
			||||||
                    e.stopPropagation();
 | 
					 | 
				
			||||||
                    $(this).addClass('is-dragging');
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.find('.contented').on('dragleave dragend', function (e) {
 | 
					 | 
				
			||||||
                    e.preventDefault();
 | 
					 | 
				
			||||||
                    e.stopPropagation();
 | 
					 | 
				
			||||||
                    $(this).removeClass('is-dragging');
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.find('.contented').on('drop', function (e) {
 | 
					 | 
				
			||||||
                    e.preventDefault();
 | 
					 | 
				
			||||||
                    e.stopPropagation();
 | 
					 | 
				
			||||||
                    handleUploadFrom(e.originalEvent.dataTransfer.files);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.find('.contented-file-upload').on('click', function(e) {
 | 
					 | 
				
			||||||
                    e.preventDefault();
 | 
					 | 
				
			||||||
                    e.stopPropagation();
 | 
					 | 
				
			||||||
                    handleUploadFrom($(".contented-file-selector")[0].files);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $f.find('.contented-paste-upload').on('click', function(e) {
 | 
					 | 
				
			||||||
                    e.preventDefault();
 | 
					 | 
				
			||||||
                    e.stopPropagation();
 | 
					 | 
				
			||||||
                    var blob = new Blob([$(".contented-if-paste textarea").val()], {type : 'text/plain'});
 | 
					 | 
				
			||||||
                    handleUploadFrom([blob]);
 | 
					 | 
				
			||||||
                });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                $("body").append($f);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var setProgressCaption = function(message) {
 | 
					 | 
				
			||||||
                    $f.find(".contented-if-progress label").text(message);
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
                var setProgressPercentage = function(frc) {
 | 
					 | 
				
			||||||
                    $f.find(".contented-progress-element").css('width', (frc * 100) + "%");
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                var handleUploadFrom = function (files) {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    setProgressCaption("Uploading, please wait...");
 | 
					 | 
				
			||||||
                    setProgressPercentage(0);
 | 
					 | 
				
			||||||
                    setType("progress");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    $f.find(".contented-upload-type-selector").hide();
 | 
					 | 
				
			||||||
                    $f.find(".contented").removeClass('is-dragging');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // Ajax uploader
 | 
					 | 
				
			||||||
                    var ajaxData = new FormData();
 | 
					 | 
				
			||||||
                    for (var i = 0; i < files.length; ++i) {
 | 
					 | 
				
			||||||
                        ajaxData.append("f", files[i]);
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    // ajax request
 | 
					 | 
				
			||||||
                    $.ajax({
 | 
					 | 
				
			||||||
                        url: baseURL + "upload",
 | 
					 | 
				
			||||||
                        type: "POST",
 | 
					 | 
				
			||||||
                        data: ajaxData,
 | 
					 | 
				
			||||||
                        dataType: 'json', // response type
 | 
					 | 
				
			||||||
                        cache: false,
 | 
					 | 
				
			||||||
                        contentType: false,
 | 
					 | 
				
			||||||
                        processData: false,
 | 
					 | 
				
			||||||
                        xhr: function() {
 | 
					 | 
				
			||||||
                            var xhr = $.ajaxSettings.xhr();
 | 
					 | 
				
			||||||
                            xhr.upload.addEventListener(
 | 
					 | 
				
			||||||
                                'progress',
 | 
					 | 
				
			||||||
                                function(ev) {
 | 
					 | 
				
			||||||
                                    console.log([ev.lengthComputable, ev.loaded, ev.total]);
 | 
					 | 
				
			||||||
                                    if (ev.lengthComputable) {
 | 
					 | 
				
			||||||
                                        setProgressCaption("Uploading (" + formatBytes(ev.loaded) + " / " + formatBytes(ev.total) + ")...");
 | 
					 | 
				
			||||||
                                        setProgressPercentage(ev.total == 0 ? 0 : ev.loaded / ev.total);
 | 
					 | 
				
			||||||
                                    }
 | 
					 | 
				
			||||||
                                },
 | 
					 | 
				
			||||||
                                false
 | 
					 | 
				
			||||||
                            );
 | 
					 | 
				
			||||||
                            return xhr;
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        complete: function () {
 | 
					 | 
				
			||||||
                            setProgressCaption("Upload complete.");
 | 
					 | 
				
			||||||
                            setProgressPercentage(1);
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        success: function (data) {
 | 
					 | 
				
			||||||
                            onUploaded(data);
 | 
					 | 
				
			||||||
                            ourClose();
 | 
					 | 
				
			||||||
                        },
 | 
					 | 
				
			||||||
                        error: function () {
 | 
					 | 
				
			||||||
                            setProgressCaption("Upload failed.");
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    //
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
        'supportsDrop': supportsDrop,
 | 
					 | 
				
			||||||
        'init': initArea
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
})(
 | 
					 | 
				
			||||||
    jQuery,
 | 
					 | 
				
			||||||
    (function () {
 | 
					 | 
				
			||||||
        "use strict";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var getCurrentScriptPath = function () {
 | 
				
			||||||
        // Determine current script path
 | 
					        // Determine current script path
 | 
				
			||||||
        // @ref https://stackoverflow.com/a/26023176
 | 
					        // @ref https://stackoverflow.com/a/26023176
 | 
				
			||||||
        var scripts = document.querySelectorAll('script[src]');
 | 
					        var scripts = document.querySelectorAll('script[src]');
 | 
				
			||||||
@@ -217,5 +32,433 @@ var contented = (function ($, currentScriptPath) {
 | 
				
			|||||||
        var currentScriptFile = currentScriptChunks[currentScriptChunks.length - 1];
 | 
					        var currentScriptFile = currentScriptChunks[currentScriptChunks.length - 1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return currentScript.replace(currentScriptFile, '');
 | 
					        return currentScript.replace(currentScriptFile, '');
 | 
				
			||||||
    })()
 | 
					    };
 | 
				
			||||||
);
 | 
						
 | 
				
			||||||
 | 
						var currentScriptPath = getCurrentScriptPath();
 | 
				
			||||||
 | 
						var baseURL = currentScriptPath.replace('sdk.js', '');
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						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 + "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"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					;(function() {
 | 
				
			||||||
 | 
						"use strict";
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						var loadScript = function(url, onLoad) {
 | 
				
			||||||
 | 
							var script = document.createElement('script');
 | 
				
			||||||
 | 
							script.onload = onLoad;
 | 
				
			||||||
 | 
							script.src = url;
 | 
				
			||||||
 | 
							document.head.appendChild(script);
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						var loadScripts = function(urls, onLoad) {
 | 
				
			||||||
 | 
							// load sequentially
 | 
				
			||||||
 | 
							var i = 0;
 | 
				
			||||||
 | 
							var loadNext = function() {
 | 
				
			||||||
 | 
								if (i === urls.length) {
 | 
				
			||||||
 | 
									onLoad();
 | 
				
			||||||
 | 
									return;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								
 | 
				
			||||||
 | 
								var url = urls[i];
 | 
				
			||||||
 | 
								i += 1;
 | 
				
			||||||
 | 
								loadScript(url, loadNext);
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
							loadNext();
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
					    var formatBytes = function(bytes) {
 | 
				
			||||||
 | 
							var k = 1024, m = (1024*1024), g = (1024*1024*1024);
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
					        if (bytes < k) {
 | 
				
			||||||
 | 
					            return bytes + " B";
 | 
				
			||||||
 | 
					        } else if (bytes < m) {
 | 
				
			||||||
 | 
					            return (bytes / k).toFixed(1) + " KiB";
 | 
				
			||||||
 | 
					        } else if (bytes < g) {
 | 
				
			||||||
 | 
					            return (bytes / m).toFixed(1) + " MiB";
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return (bytes / g).toFixed(1) + " GiB";
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						// @ref https://stackoverflow.com/a/2117523
 | 
				
			||||||
 | 
						var guid = function uuidv4() {
 | 
				
			||||||
 | 
							return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
 | 
				
			||||||
 | 
								var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
 | 
				
			||||||
 | 
								return v.toString(16);
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						var afterScriptsLoaded = function() {		
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							var initArea = function (elementSelector, onUploaded, onClose) {
 | 
				
			||||||
 | 
								onUploaded = onUploaded || function () { };
 | 
				
			||||||
 | 
								onClose = onClose || function () { };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								if ($(elementSelector).length != 1) {
 | 
				
			||||||
 | 
									return; // should only find one element
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								var element = $(elementSelector)[0];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// <input type="hidden" name="MAX_FILE_SIZE" value="` + ret.MaxUploadBytes + `" /> 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								// Create a new div for ourselves on top of the existing area
 | 
				
			||||||
 | 
								$.get(contented.baseURL + "about", function (ret) {
 | 
				
			||||||
 | 
									
 | 
				
			||||||
 | 
									var extraText = "";
 | 
				
			||||||
 | 
									if (ret.MaxUploadBytes > 0) {
 | 
				
			||||||
 | 
										extraText = " (max " + formatBytes(ret.MaxUploadBytes) + ")";
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									$.get(contented.baseURL + "widget.html", function (widgetHtml) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										var $f = $("<div>").html(widgetHtml);
 | 
				
			||||||
 | 
										$f.find(".contented-extratext").text(extraText);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Tab buttons
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										var hasSetupDrawingBoardYet = false;
 | 
				
			||||||
 | 
										var setType = function (type) {
 | 
				
			||||||
 | 
											$f.find(".contented-upload-type").removeClass("contented-upload-type-active");
 | 
				
			||||||
 | 
											$f.find(".contented-upload-type[data-upload-type=" + type + "]").addClass("contented-upload-type-active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											$f.find(".contented-upload-if").removeClass("contented-active");
 | 
				
			||||||
 | 
											$f.find(".contented-if-" + type).addClass("contented-active");
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											if (type == "drag") {
 | 
				
			||||||
 | 
												enablePasteHandler();
 | 
				
			||||||
 | 
											} else {
 | 
				
			||||||
 | 
												disablePasteHandler();
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											if (type == "drawing" && !hasSetupDrawingBoardYet) {
 | 
				
			||||||
 | 
												setupDrawingBoard();
 | 
				
			||||||
 | 
												hasSetupDrawingBoardYet = true;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										$f.find(".contented-upload-type").click(function () {
 | 
				
			||||||
 | 
											setType($(this).attr('data-upload-type'));
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Widget positioning
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										var $element = $(element);
 | 
				
			||||||
 | 
										var offset = $element.offset();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										$f.css({
 | 
				
			||||||
 | 
											'position': 'absolute',
 | 
				
			||||||
 | 
											'left': offset.left + "px",
 | 
				
			||||||
 | 
											'top': offset.top + "px",
 | 
				
			||||||
 | 
											'width': $element.width() + "px",
 | 
				
			||||||
 | 
											'min-width': $element.width() + "px",
 | 
				
			||||||
 | 
											'max-width': $element.width() + "px",
 | 
				
			||||||
 | 
											'height': $element.height() + "px",
 | 
				
			||||||
 | 
											'min-height': $element.height() + "px",
 | 
				
			||||||
 | 
											'max-height': $element.height() + "px"
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Drag and drop support
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										$f.find('.contented').on('dragover dragenter', function (e) {
 | 
				
			||||||
 | 
											e.preventDefault();
 | 
				
			||||||
 | 
											e.stopPropagation();
 | 
				
			||||||
 | 
											$(this).addClass('is-dragging');
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										$f.find('.contented').on('dragleave dragend', function (e) {
 | 
				
			||||||
 | 
											e.preventDefault();
 | 
				
			||||||
 | 
											e.stopPropagation();
 | 
				
			||||||
 | 
											$(this).removeClass('is-dragging');
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										$f.find('.contented').on('drop', function (e) {
 | 
				
			||||||
 | 
											e.preventDefault();
 | 
				
			||||||
 | 
											e.stopPropagation();
 | 
				
			||||||
 | 
											handleUploadFrom(e.originalEvent.dataTransfer.files);
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										$f.find('.contented-file-upload').on('click', function(e) {
 | 
				
			||||||
 | 
											e.preventDefault();
 | 
				
			||||||
 | 
											e.stopPropagation();
 | 
				
			||||||
 | 
											handleUploadFrom($(".contented-file-selector")[0].files);
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Pastebin
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										$f.find('.contented-paste-upload').on('click', function(e) {
 | 
				
			||||||
 | 
											e.preventDefault();
 | 
				
			||||||
 | 
											e.stopPropagation();
 | 
				
			||||||
 | 
											var blob = new Blob([$(".contented-if-paste textarea").val()], {type : 'text/plain'});
 | 
				
			||||||
 | 
											handleUploadFrom([blob]);
 | 
				
			||||||
 | 
										});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Ctrl+V uploads
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										var pasteHandler = function(e) {
 | 
				
			||||||
 | 
											e.preventDefault();
 | 
				
			||||||
 | 
											e.stopPropagation();
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											var items = (e.clipboardData || e.originalEvent.clipboardData).items;
 | 
				
			||||||
 | 
											var items_length = items.length;
 | 
				
			||||||
 | 
											var blobs = [];
 | 
				
			||||||
 | 
											var handled = 0;
 | 
				
			||||||
 | 
											var haveHandled = function() {
 | 
				
			||||||
 | 
												handled += 1;
 | 
				
			||||||
 | 
												if (handled == items_length) {
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													if (blobs.length > 0) {
 | 
				
			||||||
 | 
														handleUploadFrom( blobs );
 | 
				
			||||||
 | 
													} else {
 | 
				
			||||||
 | 
														// alert("Pasted 0 files");
 | 
				
			||||||
 | 
													}
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											};
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											for (var i = 0; i < items.length; ++i) {
 | 
				
			||||||
 | 
												var item = items[i];
 | 
				
			||||||
 | 
												var mimeType = item.type;
 | 
				
			||||||
 | 
												if (item.kind === 'file') {
 | 
				
			||||||
 | 
													blobs.push(item.getAsFile());
 | 
				
			||||||
 | 
													haveHandled();
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
												} else if (item.kind === 'string') {
 | 
				
			||||||
 | 
													item.getAsString(function(s) {
 | 
				
			||||||
 | 
														blobs.push( new Blob([s], {type : mimeType}) );
 | 
				
			||||||
 | 
														haveHandled();
 | 
				
			||||||
 | 
													});
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
												} else {
 | 
				
			||||||
 | 
													// file|string are the only supported types in
 | 
				
			||||||
 | 
													// all browsers at the time of writing
 | 
				
			||||||
 | 
													// Ignore future possibilities
 | 
				
			||||||
 | 
													haveHandled();
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										var enablePasteHandler = function() {
 | 
				
			||||||
 | 
											document.addEventListener('paste', pasteHandler);
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										var disablePasteHandler = function() {
 | 
				
			||||||
 | 
											document.removeEventListener('paste', pasteHandler);
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										// Embed in DOM, load default area
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										$("body").append($f);
 | 
				
			||||||
 | 
										if (!contented.supportsDrop()) {
 | 
				
			||||||
 | 
											setType('file');
 | 
				
			||||||
 | 
										} else {
 | 
				
			||||||
 | 
											setType('drag');
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										// Drawing board
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										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 = $("<div>").attr('id', db_id);
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											DrawingBoard.Control.ContentedUpload = DrawingBoard.Control.extend({
 | 
				
			||||||
 | 
												name: 'upload',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
												initialize: function() {
 | 
				
			||||||
 | 
													var $el = this.$el;
 | 
				
			||||||
 | 
													
 | 
				
			||||||
 | 
													$el.append('<button class="contented-drawingboard-upload">Upload</button>');
 | 
				
			||||||
 | 
													$el.on('click', '.contented-drawingboard-upload', $.proxy(function(e) {
 | 
				
			||||||
 | 
														e.preventDefault();
 | 
				
			||||||
 | 
														e.stopPropagation();
 | 
				
			||||||
 | 
														
 | 
				
			||||||
 | 
														$el.prop('disabled', true);
 | 
				
			||||||
 | 
														$el.text('Saving...');
 | 
				
			||||||
 | 
														
 | 
				
			||||||
 | 
														$db.find("canvas")[0].toBlob(function(theBlob) {
 | 
				
			||||||
 | 
															handleUploadFrom([ theBlob ]);
 | 
				
			||||||
 | 
														});
 | 
				
			||||||
 | 
													}, this));
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											$db.css({
 | 
				
			||||||
 | 
												//'width': $f.find(".contented-content-area").width(),
 | 
				
			||||||
 | 
												'height': $f.find(".contented-content-area").height(),
 | 
				
			||||||
 | 
												'overflow': 'hidden'
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
											$f.find(".contented-drawing-area").append($db);
 | 
				
			||||||
 | 
											var db = new DrawingBoard.Board(db_id, {
 | 
				
			||||||
 | 
												'controls': [
 | 
				
			||||||
 | 
													'Color',
 | 
				
			||||||
 | 
													'Size',
 | 
				
			||||||
 | 
													'DrawingMode',
 | 
				
			||||||
 | 
													'Navigation',
 | 
				
			||||||
 | 
													'ContentedUpload'
 | 
				
			||||||
 | 
												],
 | 
				
			||||||
 | 
												'controlsPosition': 'center',
 | 
				
			||||||
 | 
												'enlargeYourContainer': false,
 | 
				
			||||||
 | 
												'webStorage': false,
 | 
				
			||||||
 | 
												'droppable': false // don't mess with existing drop support
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
											
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Close button
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										var ourClose = function () {
 | 
				
			||||||
 | 
											$f.remove(); // remove from dom
 | 
				
			||||||
 | 
											disablePasteHandler();
 | 
				
			||||||
 | 
											onClose(); // upstream close
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										$f.find(".contented-close").click(function () {
 | 
				
			||||||
 | 
											ourClose();
 | 
				
			||||||
 | 
										})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Progress bar
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										var setProgressCaption = function(message) {
 | 
				
			||||||
 | 
											$f.find(".contented-if-progress label").text(message);
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
										var setProgressPercentage = function(frc) {
 | 
				
			||||||
 | 
											$f.find(".contented-progress-element").css('width', (frc * 100) + "%");
 | 
				
			||||||
 | 
										};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										// Common upload handler
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										var handleUploadFrom = function(files) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											setProgressCaption("Uploading, please wait...");
 | 
				
			||||||
 | 
											setProgressPercentage(0);
 | 
				
			||||||
 | 
											setType("progress");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											$f.find(".contented-upload-type-selector").hide();
 | 
				
			||||||
 | 
											$f.find(".contented").removeClass('is-dragging');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											// Ajax uploader
 | 
				
			||||||
 | 
											var ajaxData = new FormData();
 | 
				
			||||||
 | 
											for (var i = 0; i < files.length; ++i) {
 | 
				
			||||||
 | 
												ajaxData.append("f", files[i]);
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
											// ajax request
 | 
				
			||||||
 | 
											$.ajax({
 | 
				
			||||||
 | 
												url: contented.baseURL + "upload",
 | 
				
			||||||
 | 
												type: "POST",
 | 
				
			||||||
 | 
												data: ajaxData,
 | 
				
			||||||
 | 
												dataType: 'json', // response type
 | 
				
			||||||
 | 
												cache: false,
 | 
				
			||||||
 | 
												contentType: false,
 | 
				
			||||||
 | 
												processData: false,
 | 
				
			||||||
 | 
												xhr: function() {
 | 
				
			||||||
 | 
													var xhr = $.ajaxSettings.xhr();
 | 
				
			||||||
 | 
													xhr.upload.addEventListener(
 | 
				
			||||||
 | 
														'progress',
 | 
				
			||||||
 | 
														function(ev) {
 | 
				
			||||||
 | 
															if (ev.lengthComputable) {
 | 
				
			||||||
 | 
																setProgressCaption("Uploading (" + formatBytes(ev.loaded) + " / " + formatBytes(ev.total) + ")...");
 | 
				
			||||||
 | 
																setProgressPercentage(ev.total == 0 ? 0 : ev.loaded / ev.total);
 | 
				
			||||||
 | 
															}
 | 
				
			||||||
 | 
														},
 | 
				
			||||||
 | 
														false
 | 
				
			||||||
 | 
													);
 | 
				
			||||||
 | 
													return xhr;
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												complete: function () {
 | 
				
			||||||
 | 
													setProgressPercentage(1);
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												success: function (data) {
 | 
				
			||||||
 | 
													setProgressCaption("Upload completed successfully.");
 | 
				
			||||||
 | 
													onUploaded(data);
 | 
				
			||||||
 | 
													ourClose();
 | 
				
			||||||
 | 
												},
 | 
				
			||||||
 | 
												error: function () {
 | 
				
			||||||
 | 
													setProgressCaption("Upload failed!");
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
										
 | 
				
			||||||
 | 
										// .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// Update fields in global variable
 | 
				
			||||||
 | 
							contented.init = initArea;
 | 
				
			||||||
 | 
							contented.loaded = true;
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
							// Call initArea for all pre-initialised elements
 | 
				
			||||||
 | 
							for (var i = 0; i < contented.__preInit.length; ++i) {
 | 
				
			||||||
 | 
								initArea(contented.__preInit[i][0], contented.__preInit[i][1], contented.__preInit[i][2]);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
							
 | 
				
			||||||
 | 
						// Load scripts
 | 
				
			||||||
 | 
						var needScripts = [];
 | 
				
			||||||
 | 
						if (typeof jQuery === "undefined") {
 | 
				
			||||||
 | 
							needScripts.push(contented.baseURL + "jquery-1.12.4.min.js");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (typeof DrawingBoard === "undefined") {
 | 
				
			||||||
 | 
							needScripts.push(contented.baseURL + "drawingboard-0.4.6.min.js");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
						loadScripts(needScripts, afterScriptsLoaded);
 | 
				
			||||||
 | 
						
 | 
				
			||||||
 | 
					})()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,6 +24,7 @@
 | 
				
			|||||||
.contented .contented-upload-type-selector {
 | 
					.contented .contented-upload-type-selector {
 | 
				
			||||||
    display:block;
 | 
					    display:block;
 | 
				
			||||||
    margin-bottom: 1em;
 | 
					    margin-bottom: 1em;
 | 
				
			||||||
 | 
					    -webkit-user-select: none;
 | 
				
			||||||
    user-select: none; 
 | 
					    user-select: none; 
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.contented .contented-upload-type {
 | 
					.contented .contented-upload-type {
 | 
				
			||||||
@@ -66,7 +67,7 @@
 | 
				
			|||||||
.contented-upload-if {
 | 
					.contented-upload-if {
 | 
				
			||||||
    display:none;
 | 
					    display:none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.contented-if-paste {
 | 
					.contented-if-paste, .contented-if-drawing {
 | 
				
			||||||
    height:100%;
 | 
					    height:100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.contented-upload-if.contented-active {
 | 
					.contented-upload-if.contented-active {
 | 
				
			||||||
@@ -87,12 +88,14 @@
 | 
				
			|||||||
    border-radius:8px;
 | 
					    border-radius:8px;
 | 
				
			||||||
    background:lightgrey;
 | 
					    background:lightgrey;
 | 
				
			||||||
    position:relative;
 | 
					    position:relative;
 | 
				
			||||||
 | 
					    overflow:hidden;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
.contented-progress-element {
 | 
					.contented-progress-element {
 | 
				
			||||||
    position:absolute;
 | 
					    position:absolute;
 | 
				
			||||||
    background:darkgreen;
 | 
					    background:darkgreen;
 | 
				
			||||||
    left:0;
 | 
					    left:0;
 | 
				
			||||||
    width:0%;
 | 
					    width:0%;
 | 
				
			||||||
 | 
					    height:100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
<div class="contented">
 | 
					<div class="contented">
 | 
				
			||||||
@@ -122,11 +125,17 @@
 | 
				
			|||||||
                <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" />
 | 
					                <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>
 | 
					            </svg>
 | 
				
			||||||
        </div>
 | 
					        </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>
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <div class="contented-content-area">
 | 
					    <div class="contented-content-area">
 | 
				
			||||||
        <div class="contented-upload-if contented-if-drag contented-active">
 | 
					        <div class="contented-upload-if contented-if-drag contented-active">
 | 
				
			||||||
            <label>Drop files to upload <span class="contented-extratext"></span></label>
 | 
					            <label>Drop files or Ctrl-V to upload <span class="contented-extratext"></span></label>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="contented-upload-if contented-if-file">
 | 
					        <div class="contented-upload-if contented-if-file">
 | 
				
			||||||
@@ -140,6 +149,10 @@
 | 
				
			|||||||
            <button class="contented-paste-upload">Upload »</button>
 | 
					            <button class="contented-paste-upload">Upload »</button>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="contented-upload-if contented-if-drawing">
 | 
				
			||||||
 | 
								<div class="contented-drawing-area"></div>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        <div class="contented-upload-if contented-if-progress">
 | 
					        <div class="contented-upload-if contented-if-progress">
 | 
				
			||||||
            <label>...</label>
 | 
					            <label>...</label>
 | 
				
			||||||
            <div class="contented-progress-bar"><div class="contented-progress-element"></div></div>
 | 
					            <div class="contented-progress-bar"><div class="contented-progress-element"></div></div>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										104
									
								
								thumb.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,104 @@
 | 
				
			|||||||
 | 
					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
 | 
				
			||||||
 | 
						<-this.thumbnailSem
 | 
				
			||||||
 | 
						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
									
								
								upload.go
									
									
									
									
									
								
							
							
						
						@@ -18,15 +18,17 @@ import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
 | 
					func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	err := r.ParseMultipartForm(this.opts.MaxUploadBytes * 2)
 | 
						remoteIP := this.remoteIP(r)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						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", r.RemoteAddr, err.Error())
 | 
							log.Printf("%s Invalid request: %s\n", remoteIP, err.Error())
 | 
				
			||||||
		http.Error(w, "Invalid request", 400)
 | 
							http.Error(w, "Invalid request", 400)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File["f"]) < 1 {
 | 
						if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File["f"]) < 1 {
 | 
				
			||||||
		log.Printf("%s Invalid request: no multipart content\n", r.RemoteAddr)
 | 
							log.Printf("%s Invalid request: no multipart content\n", remoteIP)
 | 
				
			||||||
		http.Error(w, "Invalid request", 400)
 | 
							http.Error(w, "Invalid request", 400)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -36,14 +38,14 @@ func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
	for _, fhs := range r.MultipartForm.File["f"] {
 | 
						for _, fhs := range r.MultipartForm.File["f"] {
 | 
				
			||||||
		f, err := fhs.Open()
 | 
							f, err := fhs.Open()
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Printf("%s Internal error: %s\n", r.RemoteAddr, err.Error())
 | 
								log.Printf("%s Internal error: %s\n", remoteIP, err.Error())
 | 
				
			||||||
			http.Error(w, "Internal error", 500)
 | 
								http.Error(w, "Internal error", 500)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		path, err := this.handleUploadFile(f, fhs, remoteIP(r))
 | 
							path, err := this.handleUploadFile(f, fhs, remoteIP)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			log.Printf("%s Upload failed: %s\n", r.RemoteAddr, err.Error())
 | 
								log.Printf("%s Upload failed: %s\n", remoteIP, err.Error())
 | 
				
			||||||
			http.Error(w, "Upload failed", 500)
 | 
								http.Error(w, "Upload failed", 500)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +54,7 @@ func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	jb, err := json.Marshal(ret)
 | 
						jb, err := json.Marshal(ret)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		log.Printf("%s Internal error: %s\n", r.RemoteAddr, err.Error())
 | 
							log.Printf("%s Internal error: %s\n", remoteIP, err.Error())
 | 
				
			||||||
		http.Error(w, "Internal error", 500)
 | 
							http.Error(w, "Internal error", 500)
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -89,7 +91,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
 | 
				
			||||||
 
 | 
				
			|||||||