Compare commits
	
		
			29 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| bc911327cf | |||
| e04900f672 | |||
| d10026ae82 | |||
| 36b4b124f7 | |||
| 8d40031edc | |||
| 15c02efe96 | |||
| 01ff8f69aa | |||
| 3e5fb091c9 | |||
| caf521c318 | |||
| 77a4061cdd | |||
| 5ce06d6e6b | |||
| 0044d7dc77 | |||
| 6453700648 | |||
| 7407353dfb | |||
| 660038b897 | |||
| 6cef907cf9 | |||
| b959f30882 | |||
| 64b900d90c | |||
| e26a3b58b0 | |||
| 4294738337 | |||
| 156e2ab540 | |||
| ad56309cb0 | |||
| f13618fef1 | |||
| e8dd95a830 | |||
| bf3339fec7 | |||
| cb29cf83ac | |||
| ad95ac219d | |||
| 11003e010d | |||
| 6b4c2cc208 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,4 +1,5 @@
 | 
			
		||||
cmd/contented/contented
 | 
			
		||||
cmd/contented-multi/contented-multi
 | 
			
		||||
build/
 | 
			
		||||
_dist/
 | 
			
		||||
contented.db
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										30
									
								
								Makefile
									
									
									
									
									
								
							@@ -21,22 +21,12 @@ all: build/linux64/contented build/win32/contented.exe
 | 
			
		||||
 | 
			
		||||
dist: \
 | 
			
		||||
	_dist/contented-$(VERSION)-linux64.tar.gz \
 | 
			
		||||
	_dist/contented-$(VERSION)-win32.7z \
 | 
			
		||||
	_dist/contented-$(VERSION)-src.zip
 | 
			
		||||
 | 
			
		||||
clean:
 | 
			
		||||
	if [ -f ./staticResources.go ] ; then rm ./staticResources.go ; fi
 | 
			
		||||
	if [ -d ./build ] ; then rm -r ./build ; fi
 | 
			
		||||
	if [ -f ./contented ] ; then rm ./contented ; fi
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Generated files
 | 
			
		||||
#
 | 
			
		||||
 | 
			
		||||
staticResources.go: static/ static/*
 | 
			
		||||
	go-bindata -o staticResources.go -prefix static -pkg contented static
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# Release artefacts
 | 
			
		||||
#
 | 
			
		||||
@@ -48,26 +38,6 @@ build/linux64/contented: $(SOURCES) staticResources.go
 | 
			
		||||
		go build $(GOFLAGS) -o ../../build/linux64/contented \
 | 
			
		||||
	)
 | 
			
		||||
	
 | 
			
		||||
build/win32/contented.exe: $(SOURCES) staticResources.go
 | 
			
		||||
	mkdir -p build/win32
 | 
			
		||||
	(cd cmd/contented ; \
 | 
			
		||||
		PATH=/usr/lib/mxe/usr/bin:$(PATH) CC=i686-w64-mingw32.static-gcc \
 | 
			
		||||
		CGO_ENABLED=1 GOOS=windows GOARCH=386 \
 | 
			
		||||
		go build $(GOFLAGS) -o ../../build/win32/contented.exe \
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
_dist/contented-$(VERSION)-linux64.tar.gz: build/linux64/contented
 | 
			
		||||
	mkdir -p _dist
 | 
			
		||||
	tar caf _dist/contented-$(VERSION)-linux64.tar.gz -C build/linux64 contented --owner=0 --group=0
 | 
			
		||||
	
 | 
			
		||||
_dist/contented-$(VERSION)-win32.7z: build/win32/contented.exe
 | 
			
		||||
	mkdir -p _dist
 | 
			
		||||
	( cd build/win32 ; \
 | 
			
		||||
		if [ -f dist.7z ] ; then rm dist.7z ; fi ; \
 | 
			
		||||
		7z a dist.7z contented.exe ; \
 | 
			
		||||
		mv dist.7z ../../_dist/contented-$(VERSION)-win32.7z \
 | 
			
		||||
	)
 | 
			
		||||
	
 | 
			
		||||
_dist/contented-$(VERSION)-src.zip: $(SOURCES)
 | 
			
		||||
	git archive HEAD -o _dist/contented-$(VERSION)-src.zip
 | 
			
		||||
	
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								Metadata.go
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								Metadata.go
									
									
									
									
									
								
							@@ -3,13 +3,14 @@ package contented
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/boltdb/bolt"
 | 
			
		||||
	"github.com/speps/go-hashids"
 | 
			
		||||
	bolt "go.etcd.io/bbolt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
@@ -62,9 +63,17 @@ func (this *Server) AddMetadata(m Metadata) (string, error) {
 | 
			
		||||
 | 
			
		||||
	err = this.db.Update(func(tx *bolt.Tx) error {
 | 
			
		||||
		b := tx.Bucket(this.metadataBucket)
 | 
			
		||||
		seq, _ := b.NextSequence() // cannot fail
 | 
			
		||||
		seq, err := b.NextSequence()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("NextSequence: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
		shortRef = idToString(seq)
 | 
			
		||||
		return tx.Bucket(this.metadataBucket).Put([]byte(shortRef), jb)
 | 
			
		||||
 | 
			
		||||
		if b.Get([]byte(shortRef)) != nil {
 | 
			
		||||
			return fmt.Errorf("Next bucket sequence %d creates colliding ID %s", seq, shortRef)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return b.Put([]byte(shortRef), jb)
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										41
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								README.md
									
									
									
									
									
								
							@@ -6,6 +6,8 @@ A file / image / paste upload server with a focus on embedding.
 | 
			
		||||
 | 
			
		||||
You can use contented as a standalone upload server, or you can use the SDK to embed its upload widget into another website.
 | 
			
		||||
 | 
			
		||||
The name is a pun on "content" and the -d suffix for server daemons.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- Drag and drop upload
 | 
			
		||||
@@ -13,6 +15,7 @@ You can use contented as a standalone upload server, or you can use the SDK to e
 | 
			
		||||
- Pastebin upload
 | 
			
		||||
- Custom drawing upload ([via drawingboard.js](https://github.com/Leimi/drawingboard.js))
 | 
			
		||||
- Ctrl-V upload
 | 
			
		||||
- Galleries
 | 
			
		||||
- SDK-oriented design for embedding, including CORS support
 | 
			
		||||
- Mobile friendly HTML interface
 | 
			
		||||
- Preserves uploaded filename and content-type metadata
 | 
			
		||||
@@ -26,14 +29,18 @@ You can use contented as a standalone upload server, or you can use the SDK to e
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
Usage of contented:
 | 
			
		||||
  -concurrentthumbs int
 | 
			
		||||
        Simultaneous thumbnail generation (default 16)
 | 
			
		||||
  -data string
 | 
			
		||||
        Directory for stored content (default "")
 | 
			
		||||
        Directory for stored content (default ".")
 | 
			
		||||
  -db string
 | 
			
		||||
        Path for metadata database (default "contented.db")
 | 
			
		||||
  -diskFilesWorldReadable
 | 
			
		||||
        Save files as 0644 instead of 0600
 | 
			
		||||
  -enableHomepage
 | 
			
		||||
        Enable homepage (disable for embedded use only) (default true)
 | 
			
		||||
  -enableUpload
 | 
			
		||||
        Enable uploads (disable for read-only mode) (default true)
 | 
			
		||||
  -listen string
 | 
			
		||||
        IP/Port to bind server (default "127.0.0.1:80")
 | 
			
		||||
  -max int
 | 
			
		||||
@@ -44,8 +51,6 @@ Usage of contented:
 | 
			
		||||
        Title used in web interface (default "contented")
 | 
			
		||||
  -trustXForwardedFor
 | 
			
		||||
        Trust X-Forwarded-For reverse proxy headers
 | 
			
		||||
  -concurrentthumbs
 | 
			
		||||
        Simultaneous thumbnail generation (default 16)
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If you are hosting behind a reverse proxy, remember to set its post body size parameter appropriately (e.g. `client_max_body_size` for nginx).
 | 
			
		||||
@@ -57,21 +62,45 @@ The server responds on the following URLs:
 | 
			
		||||
URL                  |Method |Description
 | 
			
		||||
---------------------|-------|---
 | 
			
		||||
`/get/{ID}`          |`GET`  |Download item content
 | 
			
		||||
`/p/{ID}`            |`GET`  |Preview item content (HTML)
 | 
			
		||||
`/p/{ID}-{ID}-...`   |`GET`  |Preview multiple item content (HTML)
 | 
			
		||||
`/info/{ID}`         |`GET`  |Get item content metadata (JSON)
 | 
			
		||||
`/thumb/{Type}/{ID}` |`GET`  |Get item thumbnail image
 | 
			
		||||
`/thumb/{Type}/{ID}` |`GET`  |Get item thumbnail image (JPEG). "Type" should match `[sbtmlh]`.
 | 
			
		||||
`/about`             |`GET`  |Get server metadata (JSON)
 | 
			
		||||
 | 
			
		||||
## Usage (Embedding for web)
 | 
			
		||||
 | 
			
		||||
Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element. Your callback will be passed an array of file IDs of any uploaded items.
 | 
			
		||||
Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element:
 | 
			
		||||
 | 
			
		||||
```html
 | 
			
		||||
<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
 | 
			
		||||
contented.init("#target", function(/* String[] */ items) {});
 | 
			
		||||
contented.init("#target");
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
You can optionally supply additional ordered parameters to `contented.init`:
 | 
			
		||||
 | 
			
		||||
1. A callback, that will be passed an array of file IDs of any uploaded items
 | 
			
		||||
2. A callback, that will be called if the SDK widget is closed
 | 
			
		||||
 | 
			
		||||
## Changelog
 | 
			
		||||
 | 
			
		||||
2023-05-19: 1.5.0
 | 
			
		||||
- Feature: Support S3-backed storage
 | 
			
		||||
- Feature: New `contented-multi` binary to host multiple server configurations from a single process
 | 
			
		||||
- Enhancement: Better client-side caching for thumbnails
 | 
			
		||||
- Option to cap source filesize for thumbnailing (default 20MiB)
 | 
			
		||||
 | 
			
		||||
2023-05-17: 1.4.0
 | 
			
		||||
- BREAKING: Remove support for some old web browsers (require jQuery 3, ES6 template literals, Promises, Canvas.toBlob)
 | 
			
		||||
- Feature: Initial album support with custom titles
 | 
			
		||||
- Feature: Support readonly mode
 | 
			
		||||
- Enhancement: Use lazy-loading for large image galleries
 | 
			
		||||
- Enhancement: Better tab titles on preview pages
 | 
			
		||||
- Fix an issue with continuing server-side thumbnailing work even if the http client has gone away
 | 
			
		||||
- Fix an issue with not warning on colliding hashIDs
 | 
			
		||||
- Internal: Refactor the SDK's initialization phase
 | 
			
		||||
- Internal: Update bolt library dependency
 | 
			
		||||
 | 
			
		||||
2020-07-25: 1.3.1
 | 
			
		||||
- Fix an issue with dependencies causing failure to compile in Modules mode
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										70
									
								
								Server.go
									
									
									
									
									
								
							
							
						
						
									
										70
									
								
								Server.go
									
									
									
									
									
								
							@@ -1,8 +1,10 @@
 | 
			
		||||
package contented
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
@@ -10,13 +12,26 @@ import (
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/boltdb/bolt"
 | 
			
		||||
	"github.com/minio/minio-go/v7"
 | 
			
		||||
	"github.com/minio/minio-go/v7/pkg/credentials"
 | 
			
		||||
	"github.com/mxk/go-flowrate/flowrate"
 | 
			
		||||
	bolt "go.etcd.io/bbolt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//go:embed static
 | 
			
		||||
var staticAssets embed.FS
 | 
			
		||||
 | 
			
		||||
var SERVER_HEADER string = `contented/0.0.0-dev`
 | 
			
		||||
 | 
			
		||||
const DEFAULT_MAX_CONCURRENT_THUMBS = 16
 | 
			
		||||
const (
 | 
			
		||||
	DEFAULT_MAX_CONCURRENT_THUMBS = 16
 | 
			
		||||
	DEFAULT_MAX_THUMBSIZE         = 20 * 1024 * 1024 // 20 MiB
 | 
			
		||||
 | 
			
		||||
	ALBUM_MIMETYPE = `contented/album`
 | 
			
		||||
 | 
			
		||||
	STORAGE_LOCAL int = 0
 | 
			
		||||
	STORAGE_S3    int = 1
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ServerPublicProperties struct {
 | 
			
		||||
	AppTitle         string
 | 
			
		||||
@@ -25,13 +40,24 @@ type ServerPublicProperties struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ServerOptions struct {
 | 
			
		||||
	StorageType   int // STORAGE_xx
 | 
			
		||||
	DataDirectory string
 | 
			
		||||
	DataS3Options struct {
 | 
			
		||||
		Hostname  string
 | 
			
		||||
		AccessKey string
 | 
			
		||||
		SecretKey string
 | 
			
		||||
		Bucket    string
 | 
			
		||||
		Prefix    string
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	DBPath                 string
 | 
			
		||||
	DiskFilesWorldReadable bool
 | 
			
		||||
	BandwidthLimit         int64
 | 
			
		||||
	TrustXForwardedFor     bool
 | 
			
		||||
	EnableHomepage         bool
 | 
			
		||||
	EnableUpload           bool
 | 
			
		||||
	MaxConcurrentThumbs    int
 | 
			
		||||
	MaxThumbSizeBytes      int64
 | 
			
		||||
	ServerPublicProperties
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -45,10 +71,12 @@ func (this *ServerOptions) FileMode() os.FileMode {
 | 
			
		||||
 | 
			
		||||
type Server struct {
 | 
			
		||||
	opts           ServerOptions
 | 
			
		||||
	s3client       *minio.Client
 | 
			
		||||
	db             *bolt.DB
 | 
			
		||||
	startTime      time.Time
 | 
			
		||||
	thumbnailSem   chan struct{}
 | 
			
		||||
	metadataBucket []byte
 | 
			
		||||
	staticDir      fs.FS // interface
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewServer(opts *ServerOptions) (*Server, error) {
 | 
			
		||||
@@ -63,6 +91,26 @@ func NewServer(opts *ServerOptions) (*Server, error) {
 | 
			
		||||
		log.Printf("Allowing %d concurrent thumbnails", s.opts.MaxConcurrentThumbs)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.opts.MaxThumbSizeBytes <= 0 {
 | 
			
		||||
		s.opts.MaxThumbSizeBytes = DEFAULT_MAX_THUMBSIZE
 | 
			
		||||
		log.Printf("Allowing thumbnails for files up to %d byte(s)", s.opts.MaxThumbSizeBytes)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	s.staticDir, _ = fs.Sub(staticAssets, `static`) // can't fail
 | 
			
		||||
 | 
			
		||||
	// Maybe open s3 connection
 | 
			
		||||
	if s.opts.StorageType == STORAGE_S3 {
 | 
			
		||||
		cl, err := minio.New(opts.DataS3Options.Hostname, &minio.Options{
 | 
			
		||||
			Creds:  credentials.NewStaticV4(opts.DataS3Options.AccessKey, opts.DataS3Options.SecretKey, ""),
 | 
			
		||||
			Secure: true,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, fmt.Errorf("Connecting to S3 host: %w", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		s.s3client = cl
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// "fill" the thumbnailer semaphore
 | 
			
		||||
	s.thumbnailSem = make(chan struct{}, s.opts.MaxConcurrentThumbs)
 | 
			
		||||
	for i := 0; i < s.opts.MaxConcurrentThumbs; i += 1 {
 | 
			
		||||
@@ -141,7 +189,7 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		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):])
 | 
			
		||||
		this.handlePreview(r.Context(), w, r.URL.Path[len(previewUrlPrefix):])
 | 
			
		||||
 | 
			
		||||
	} else if r.Method == "GET" && rxThumbUrl.MatchString(r.URL.Path) {
 | 
			
		||||
		parts := rxThumbUrl.FindStringSubmatch(r.URL.Path)
 | 
			
		||||
@@ -157,11 +205,17 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
		// Blanket allow (headers already set)
 | 
			
		||||
		w.WriteHeader(200)
 | 
			
		||||
 | 
			
		||||
	} else if r.Method == "GET" && r.URL.Path == `/` && this.opts.EnableHomepage {
 | 
			
		||||
		http.Redirect(w, r, `/index.html`, http.StatusFound)
 | 
			
		||||
	} else if r.Method == "GET" {
 | 
			
		||||
 | 
			
		||||
	} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" && (this.opts.EnableHomepage || r.URL.Path != `/index.html`) {
 | 
			
		||||
		http.ServeContent(w, r, r.URL.Path[1:], this.startTime, bytes.NewReader(static))
 | 
			
		||||
		// Conditionally block homepage access
 | 
			
		||||
		if !this.opts.EnableHomepage && (r.URL.Path == `/index.html` || r.URL.Path == `/`) {
 | 
			
		||||
			http.Error(w, "Not found", 404)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// Serve static html/css/js assets
 | 
			
		||||
		// http.FileServer transparently redirects index.html->/ internally
 | 
			
		||||
		http.FileServer(http.FS(this.staticDir)).ServeHTTP(w, r)
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		http.Error(w, "Not found", 404)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								cmd/contented-multi/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								cmd/contented-multi/main.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"flag"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"code.ivysaur.me/contented"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ContentedMultiCfg struct {
 | 
			
		||||
	Servers []struct {
 | 
			
		||||
		ListenAddr string
 | 
			
		||||
		Options    contented.ServerOptions
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	configFile := flag.String("config", "contented-multi.cfg", "Path to configuration file")
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	fh, err := os.Open(*configFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var cfg ContentedMultiCfg
 | 
			
		||||
	err = json.NewDecoder(fh).Decode(&cfg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fh.Close()
 | 
			
		||||
 | 
			
		||||
	wg := sync.WaitGroup{}
 | 
			
		||||
	wg.Add(len(cfg.Servers))
 | 
			
		||||
 | 
			
		||||
	for i, _ := range cfg.Servers {
 | 
			
		||||
		go (func(i int) {
 | 
			
		||||
			defer wg.Done()
 | 
			
		||||
 | 
			
		||||
			s, err := contented.NewServer(&cfg.Servers[i].Options)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Printf("Failed to create server %d/%d: %s", i+1, len(cfg.Servers), err.Error())
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = http.ListenAndServe(cfg.Servers[i].ListenAddr, s)
 | 
			
		||||
			log.Printf("Server %d/%d shutting down: %s", i+1, len(cfg.Servers), err.Error())
 | 
			
		||||
 | 
			
		||||
		})(i)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wg.Wait()
 | 
			
		||||
}
 | 
			
		||||
@@ -12,32 +12,59 @@ import (
 | 
			
		||||
func main() {
 | 
			
		||||
	cwd, _ := os.Getwd()
 | 
			
		||||
 | 
			
		||||
	listenAddr := flag.String("listen", "127.0.0.1:80", "IP/Port to bind server")
 | 
			
		||||
	dataDir := flag.String("data", cwd, "Directory for stored content")
 | 
			
		||||
	dbPath := flag.String("db", "contented.db", "Path for metadata database")
 | 
			
		||||
	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)")
 | 
			
		||||
	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")
 | 
			
		||||
	var (
 | 
			
		||||
		listenAddr             = flag.String("listen", "127.0.0.1:80", "IP/Port to bind server")
 | 
			
		||||
		dataDir                = flag.String("data", cwd, "Directory for stored content")
 | 
			
		||||
		dbPath                 = flag.String("db", "contented.db", "Path for metadata database")
 | 
			
		||||
		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)")
 | 
			
		||||
		maxUploadSpeed         = flag.Int("speed", 0, "Maximum upload speed in bytes/sec (set zero for unlimited)")
 | 
			
		||||
		trustXForwardedFor     = flag.Bool("trustXForwardedFor", false, "Trust X-Forwarded-For reverse proxy headers")
 | 
			
		||||
		enableHomepage         = flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)")
 | 
			
		||||
		enableUpload           = flag.Bool("enableUpload", true, "Enable uploads (disable for read-only mode)")
 | 
			
		||||
		diskFilesWorldReadable = flag.Bool("diskFilesWorldReadable", false, "Save files as 0644 instead of 0600")
 | 
			
		||||
		maxConcurrentThumbs    = flag.Int("concurrentthumbs", contented.DEFAULT_MAX_CONCURRENT_THUMBS, "Simultaneous thumbnail generation")
 | 
			
		||||
		s3Host                 = flag.String("s3hostname", "", "S3 Server hostname")
 | 
			
		||||
		s3AccessKey            = flag.String("s3access", "", "S3 Access key")
 | 
			
		||||
		s3SecretKey            = flag.String("s3secret", "", "S3 Secret key")
 | 
			
		||||
		s3Bucket               = flag.String("s3bucket", "", "S3 Bucket")
 | 
			
		||||
		s3Prefix               = flag.String("s3prefix", "", "S3 object prefix")
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	flag.Parse()
 | 
			
		||||
 | 
			
		||||
	svr, err := contented.NewServer(&contented.ServerOptions{
 | 
			
		||||
		DataDirectory:          *dataDir,
 | 
			
		||||
	opts := contented.ServerOptions{
 | 
			
		||||
		DBPath:                 *dbPath,
 | 
			
		||||
		BandwidthLimit:         int64(*maxUploadSpeed),
 | 
			
		||||
		TrustXForwardedFor:     *trustXForwardedFor,
 | 
			
		||||
		EnableHomepage:         *enableHomepage,
 | 
			
		||||
		EnableUpload:           *enableUpload,
 | 
			
		||||
		DiskFilesWorldReadable: *diskFilesWorldReadable,
 | 
			
		||||
		MaxConcurrentThumbs:    *maxConcurrentThumbs,
 | 
			
		||||
		ServerPublicProperties: contented.ServerPublicProperties{
 | 
			
		||||
			AppTitle:       *appTitle,
 | 
			
		||||
			MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024,
 | 
			
		||||
		},
 | 
			
		||||
	})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(*s3AccessKey) > 0 {
 | 
			
		||||
		opts.StorageType = contented.STORAGE_S3
 | 
			
		||||
		opts.DataS3Options.Hostname = *s3Host
 | 
			
		||||
		opts.DataS3Options.AccessKey = *s3AccessKey
 | 
			
		||||
		opts.DataS3Options.SecretKey = *s3SecretKey
 | 
			
		||||
		opts.DataS3Options.Bucket = *s3Bucket
 | 
			
		||||
		opts.DataS3Options.Prefix = *s3Prefix
 | 
			
		||||
 | 
			
		||||
	} else if len(*dataDir) > 0 {
 | 
			
		||||
		opts.StorageType = contented.STORAGE_LOCAL
 | 
			
		||||
		opts.DataDirectory = *dataDir
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Println("Please specify either the -data or -s3__ options.")
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	svr, err := contented.NewServer(&opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Println(err.Error())
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								download.go
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								download.go
									
									
									
									
									
								
							@@ -4,7 +4,6 @@ import (
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (this *Server) handleView(w http.ResponseWriter, r *http.Request, fileID string) {
 | 
			
		||||
@@ -28,7 +27,7 @@ func (this *Server) handleViewInternal(w http.ResponseWriter, r *http.Request, f
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Load file
 | 
			
		||||
	f, err := os.Open(filepath.Join(this.opts.DataDirectory, m.FileHash))
 | 
			
		||||
	f, err := this.ReadFile(r.Context(), m.FileHash)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@@ -50,7 +49,18 @@ func (this *Server) handleViewInternal(w http.ResponseWriter, r *http.Request, f
 | 
			
		||||
		w.Header().Set(`Content-Type`, m.MimeType)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	http.ServeContent(w, r, "", m.UploadTime, f)
 | 
			
		||||
	/*
 | 
			
		||||
		if _, ok := f.(io.ReadSeeker); ! ok {
 | 
			
		||||
			// Stream directly, no support for bytes/etag
 | 
			
		||||
			w.Header().Set(`Content-Length`, fmt.Sprintf("%d", m.FileSize))
 | 
			
		||||
			_, err := io.Copy(w, f)
 | 
			
		||||
			return err
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	// Allow range requests, if-modified-since, and so on
 | 
			
		||||
	http.ServeContent(w, r, "", m.UploadTime, f)
 | 
			
		||||
	return nil
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								go.mod
									
									
									
									
									
								
							@@ -2,10 +2,32 @@ module code.ivysaur.me/contented
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	code.ivysaur.me/thumbnail v1.0.2
 | 
			
		||||
	github.com/boltdb/bolt v1.3.1
 | 
			
		||||
	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f
 | 
			
		||||
	github.com/speps/go-hashids v1.0.0
 | 
			
		||||
	golang.org/x/sys v0.0.0-20180606202747-9527bec2660b // indirect
 | 
			
		||||
	go.etcd.io/bbolt v1.3.7
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
go 1.13
 | 
			
		||||
require (
 | 
			
		||||
	code.ivysaur.me/imagequant/v2 v2.12.6 // indirect
 | 
			
		||||
	github.com/dustin/go-humanize v1.0.1 // indirect
 | 
			
		||||
	github.com/google/uuid v1.3.0 // indirect
 | 
			
		||||
	github.com/hashicorp/golang-lru v0.5.4 // indirect
 | 
			
		||||
	github.com/json-iterator/go v1.1.12 // indirect
 | 
			
		||||
	github.com/klauspost/compress v1.16.0 // indirect
 | 
			
		||||
	github.com/klauspost/cpuid/v2 v2.2.4 // indirect
 | 
			
		||||
	github.com/minio/md5-simd v1.1.2 // indirect
 | 
			
		||||
	github.com/minio/minio-go/v7 v7.0.52 // indirect
 | 
			
		||||
	github.com/minio/sha256-simd v1.0.0 // indirect
 | 
			
		||||
	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 | 
			
		||||
	github.com/modern-go/reflect2 v1.0.2 // indirect
 | 
			
		||||
	github.com/rs/xid v1.4.0 // indirect
 | 
			
		||||
	github.com/sirupsen/logrus v1.9.0 // indirect
 | 
			
		||||
	golang.org/x/crypto v0.6.0 // indirect
 | 
			
		||||
	golang.org/x/image v0.0.0-20200618115811-c13761719519 // indirect
 | 
			
		||||
	golang.org/x/net v0.7.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.5.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.7.0 // indirect
 | 
			
		||||
	gopkg.in/ini.v1 v1.67.0 // indirect
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
go 1.19
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								go.sum
									
									
									
									
									
								
							@@ -4,14 +4,66 @@ 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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 | 
			
		||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 | 
			
		||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 | 
			
		||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 | 
			
		||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 | 
			
		||||
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 | 
			
		||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 | 
			
		||||
github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
 | 
			
		||||
github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
 | 
			
		||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
 | 
			
		||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
 | 
			
		||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
 | 
			
		||||
github.com/minio/minio-go/v7 v7.0.52 h1:8XhG36F6oKQUDDSuz6dY3rioMzovKjW40W6ANuN0Dps=
 | 
			
		||||
github.com/minio/minio-go/v7 v7.0.52/go.mod h1:IbbodHyjUAguneyucUaahv+VMNs/EOTV9du7A7/Z3HU=
 | 
			
		||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
 | 
			
		||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 | 
			
		||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
 | 
			
		||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
 | 
			
		||||
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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 | 
			
		||||
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
 | 
			
		||||
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
 | 
			
		||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
 | 
			
		||||
github.com/speps/go-hashids v1.0.0 h1:jdFC07PrExRM4Og5Ev4411Tox75aFpkC77NlmutadNI=
 | 
			
		||||
github.com/speps/go-hashids v1.0.0/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
 | 
			
		||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 | 
			
		||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 | 
			
		||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 | 
			
		||||
go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
 | 
			
		||||
go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
 | 
			
		||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
 | 
			
		||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
 | 
			
		||||
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/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
 | 
			
		||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b h1:5rOiLYVqtE+JehJPVJTXQJaP8aT3cpJC1Iy22+5WLFU=
 | 
			
		||||
golang.org/x/sys v0.0.0-20180606202747-9527bec2660b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
 | 
			
		||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
 | 
			
		||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 | 
			
		||||
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
 | 
			
		||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
 | 
			
		||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 | 
			
		||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
 | 
			
		||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
 | 
			
		||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										85
									
								
								preview.go
									
									
									
									
									
								
							
							
						
						
									
										85
									
								
								preview.go
									
									
									
									
									
								
							@@ -1,6 +1,8 @@
 | 
			
		||||
package contented
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"html"
 | 
			
		||||
	"log"
 | 
			
		||||
@@ -10,14 +12,34 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (this *Server) handlePreview(w http.ResponseWriter, fileIDList string) {
 | 
			
		||||
func (this *Server) handlePreview(ctx context.Context, w http.ResponseWriter, fileIDList string) {
 | 
			
		||||
 | 
			
		||||
	fileIDs := strings.Split(fileIDList, `-`)
 | 
			
		||||
 | 
			
		||||
	// Early get metadata for the first listed element
 | 
			
		||||
	specialTitle := ""
 | 
			
		||||
	if len(fileIDs) == 1 {
 | 
			
		||||
		mFirst, err := this.Metadata(fileIDs[0])
 | 
			
		||||
		if err != nil { // Same error handling as below -
 | 
			
		||||
			if os.IsNotExist(err) {
 | 
			
		||||
				http.Error(w, "Not found", 404)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.Println(err.Error())
 | 
			
		||||
			http.Error(w, "Internal error", 500)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		specialTitle = mFirst.Filename + " (" + fileIDs[0] + ")"
 | 
			
		||||
	} else {
 | 
			
		||||
		specialTitle = fmt.Sprintf("%d images", len(fileIDs))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tmpl := `<!DOCTYPE html>
 | 
			
		||||
<html prefix="og: http://ogp.me/ns#">
 | 
			
		||||
	<head>
 | 
			
		||||
		<title>` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `</title>
 | 
			
		||||
		<title>` + html.EscapeString(specialTitle+" | "+this.opts.ServerPublicProperties.AppTitle) + `</title>
 | 
			
		||||
		<meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
		<meta property="og:title" content="` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `" />
 | 
			
		||||
		<meta property="og:site_name" content="` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `" />
 | 
			
		||||
@@ -55,7 +77,21 @@ html, body {
 | 
			
		||||
.thumbnail {
 | 
			
		||||
	line-height: 0;
 | 
			
		||||
	width: 340px;
 | 
			
		||||
	height: 340px;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	position: relative;
 | 
			
		||||
}
 | 
			
		||||
.thumbnail-overlay {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	bottom: 4px;
 | 
			
		||||
	right: 4px;
 | 
			
		||||
	padding: 4px 8px;
 | 
			
		||||
	line-height: 1.5em;
 | 
			
		||||
	
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
	
 | 
			
		||||
	background: red;
 | 
			
		||||
	color: white;
 | 
			
		||||
}
 | 
			
		||||
.properties {
 | 
			
		||||
	background: #000;
 | 
			
		||||
@@ -82,10 +118,52 @@ html, body {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if m.MimeType == ALBUM_MIMETYPE {
 | 
			
		||||
			// Special handling for albums
 | 
			
		||||
			f, err := this.ReadFile(ctx, m.FileHash)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Printf("Opening file '%s' for preview of album '%s': %s", m.FileHash, fileID, err.Error())
 | 
			
		||||
				http.Error(w, "Internal error", 500)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var childIDs []string
 | 
			
		||||
			err = json.NewDecoder(f).Decode(&childIDs)
 | 
			
		||||
			f.Close()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Printf("Failed to parse album '%s': %s", fileID, err)
 | 
			
		||||
				http.Error(w, "Internal error", 500)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(childIDs) == 0 {
 | 
			
		||||
				log.Printf("Failed to parse album '%s': no entries in album", fileID)
 | 
			
		||||
				http.Error(w, "Internal error", 500)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			tmpl += `
 | 
			
		||||
				<div class="entry">			
 | 
			
		||||
					<div class="thumbnail">
 | 
			
		||||
					<a href="` + html.EscapeString(`/get/`+fileID) + `"><img src="` + html.EscapeString(`/thumb/m/`+fileID) + `"></a>
 | 
			
		||||
						<a href="` + html.EscapeString(`/p/`+strings.Join(childIDs, `-`)) + `"><img loading="lazy" src="` + html.EscapeString(`/thumb/m/`+childIDs[0]) + `"></a>
 | 
			
		||||
						<div class="thumbnail-overlay">` + fmt.Sprintf("%d", len(childIDs)) + ` image(s)</div>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="properties">
 | 
			
		||||
						<b>Name:</b> ` + html.EscapeString(m.Filename) + `<br>
 | 
			
		||||
						<b>Hash:</b> <span title="` + html.EscapeString(m.FileHash) + `">hover</span><br>
 | 
			
		||||
						<b>File type:</b> Album<br>
 | 
			
		||||
						<b>Size:</b> ` + fmt.Sprintf("%d", len(childIDs)) + ` image(s)<br>
 | 
			
		||||
						<b>Uploader:</b> ` + html.EscapeString(m.UploadIP) + `<br>
 | 
			
		||||
						<b>Uploaded at:</b> ` + html.EscapeString(m.UploadTime.Format(time.RFC3339)) + `<br>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			`
 | 
			
		||||
 | 
			
		||||
		} else {
 | 
			
		||||
			tmpl += `
 | 
			
		||||
				<div class="entry">			
 | 
			
		||||
					<div class="thumbnail">
 | 
			
		||||
						<a href="` + html.EscapeString(`/get/`+fileID) + `"><img loading="lazy" src="` + html.EscapeString(`/thumb/m/`+fileID) + `"></a>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div class="properties">
 | 
			
		||||
						<b>Name:</b> ` + html.EscapeString(m.Filename) + `<br>
 | 
			
		||||
@@ -98,6 +176,7 @@ html, body {
 | 
			
		||||
				</div>
 | 
			
		||||
			`
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if this.opts.EnableHomepage {
 | 
			
		||||
		tmpl += `
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,7 @@ button.again {
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
        
 | 
			
		||||
        <script type="text/javascript" src="/jquery-1.12.4.min.js"></script>
 | 
			
		||||
        <script type="text/javascript" src="/jquery-3.7.0.min.js"></script>
 | 
			
		||||
		<script type="text/javascript" src="/sdk.js"></script>
 | 
			
		||||
        <script type="text/javascript">
 | 
			
		||||
"use strict";
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								static/jquery-1.12.4.min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								static/jquery-1.12.4.min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										2
									
								
								static/jquery-3.7.0.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								static/jquery-3.7.0.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										428
									
								
								static/sdk.js
									
									
									
									
									
								
							
							
						
						
									
										428
									
								
								static/sdk.js
									
									
									
									
									
								
							@@ -1,28 +1,6 @@
 | 
			
		||||
;
 | 
			
		||||
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
var contented = (function() {
 | 
			
		||||
;(function() {
 | 
			
		||||
	"use strict";
 | 
			
		||||
	
 | 
			
		||||
	// @ref https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob 
 | 
			
		||||
	if (!HTMLCanvasElement.prototype.toBlob) {
 | 
			
		||||
		Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
 | 
			
		||||
			value: function (callback, type, quality) {
 | 
			
		||||
 | 
			
		||||
				var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
 | 
			
		||||
					len = binStr.length,
 | 
			
		||||
					arr = new Uint8Array(len);
 | 
			
		||||
 | 
			
		||||
				for (var i = 0; i < len; i++ ) {
 | 
			
		||||
					arr[i] = binStr.charCodeAt(i);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				callback( new Blob( [arr], {type: type || 'image/png'} ) );
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var getCurrentScriptPath = function () {
 | 
			
		||||
		// Determine current script path
 | 
			
		||||
		// @ref https://stackoverflow.com/a/26023176
 | 
			
		||||
@@ -34,87 +12,14 @@ var contented = (function() {
 | 
			
		||||
		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 loadScript = function(url) {
 | 
			
		||||
		return new Promise(function(resolve, reject) {
 | 
			
		||||
			var script = document.createElement('script');
 | 
			
		||||
		script.onload = onLoad;
 | 
			
		||||
			script.onload = resolve;
 | 
			
		||||
			script.onerror = reject;
 | 
			
		||||
			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) {
 | 
			
		||||
@@ -139,9 +44,184 @@ var contented = (function() {
 | 
			
		||||
		});
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	var afterScriptsLoaded = function() {		
 | 
			
		||||
	var widgetHtml = `
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
	.contented {
 | 
			
		||||
		box-sizing:border-box;
 | 
			
		||||
		text-align: center;
 | 
			
		||||
		border: 8px dashed lightgrey;
 | 
			
		||||
		padding: 12px;
 | 
			
		||||
		background:white; /* not transparent */
 | 
			
		||||
	
 | 
			
		||||
		var initArea = function (elementSelector, onUploaded, onClose) {
 | 
			
		||||
		text-overflow:hidden;
 | 
			
		||||
		overflow:auto;
 | 
			
		||||
		width:100%;
 | 
			
		||||
		height:100%;
 | 
			
		||||
	
 | 
			
		||||
		position:relative;
 | 
			
		||||
	}
 | 
			
		||||
	.contented .contented-close {
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: 12px;
 | 
			
		||||
		right: 12px;
 | 
			
		||||
		width: 24px;
 | 
			
		||||
		height: 24px;
 | 
			
		||||
		cursor: pointer;
 | 
			
		||||
	}
 | 
			
		||||
	.contented .contented-upload-type-selector {
 | 
			
		||||
		display:block;
 | 
			
		||||
		margin-bottom: 1em;
 | 
			
		||||
		-webkit-user-select: none;
 | 
			
		||||
		user-select: none; 
 | 
			
		||||
	}
 | 
			
		||||
	.contented .contented-upload-type {
 | 
			
		||||
		display:inline-block;
 | 
			
		||||
		opacity:0.2;
 | 
			
		||||
		transition:opacity linear 0.1s;
 | 
			
		||||
		cursor:pointer;
 | 
			
		||||
	}
 | 
			
		||||
	.contented .contented-upload-type:hover {
 | 
			
		||||
		opacity:0.5;
 | 
			
		||||
		transition:opacity linear 0s;
 | 
			
		||||
	}
 | 
			
		||||
	.contented .contented-upload-type svg {
 | 
			
		||||
		width:36px;
 | 
			
		||||
		height:36px;
 | 
			
		||||
	}
 | 
			
		||||
	.contented .contented-upload-type.contented-upload-type-active {
 | 
			
		||||
		opacity:1;
 | 
			
		||||
	}
 | 
			
		||||
	.contented.is-dragging {
 | 
			
		||||
		background: lightblue;
 | 
			
		||||
	}
 | 
			
		||||
	.contented-content-area {
 | 
			
		||||
		position:absolute;
 | 
			
		||||
		top: 60px;
 | 
			
		||||
		bottom: 10px;
 | 
			
		||||
		left: 10px;
 | 
			
		||||
		width: calc(100% - 20px);
 | 
			
		||||
	
 | 
			
		||||
		/* Prevent blur under translateY */
 | 
			
		||||
		-webkit-transform-style: preserve-3d;
 | 
			
		||||
		-moz-transform-style: preserve-3d;
 | 
			
		||||
		transform-style: preserve-3d;
 | 
			
		||||
	}
 | 
			
		||||
	.contented-content-area > div {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		top: 50%;
 | 
			
		||||
		transform: translateY(-50%);
 | 
			
		||||
	}
 | 
			
		||||
	.contented-upload-if {
 | 
			
		||||
		display:none;
 | 
			
		||||
	}
 | 
			
		||||
	.contented-if-paste, .contented-if-drawing {
 | 
			
		||||
		height:100%;
 | 
			
		||||
	}
 | 
			
		||||
	.contented-upload-if.contented-active {
 | 
			
		||||
		display:block;
 | 
			
		||||
	}
 | 
			
		||||
	.contented textarea {
 | 
			
		||||
		resize: none;
 | 
			
		||||
		width:100%;
 | 
			
		||||
		height:calc(100% - 1em - 15px);
 | 
			
		||||
		box-sizing:border-box;
 | 
			
		||||
	}
 | 
			
		||||
	.contented-progress-bar {
 | 
			
		||||
		display: block;
 | 
			
		||||
		width:90%;
 | 
			
		||||
		margin:0.5em auto 0 auto;
 | 
			
		||||
	
 | 
			
		||||
		height:16px;
 | 
			
		||||
		border-radius:8px;
 | 
			
		||||
		background:lightgrey;
 | 
			
		||||
		position:relative;
 | 
			
		||||
		overflow:hidden;
 | 
			
		||||
	}
 | 
			
		||||
	.contented-progress-element {
 | 
			
		||||
		position:absolute;
 | 
			
		||||
		background:darkgreen;
 | 
			
		||||
		left:0;
 | 
			
		||||
		width:0%;
 | 
			
		||||
		height:100%;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
<div class="contented">
 | 
			
		||||
 | 
			
		||||
	<div class="contented-close">
 | 
			
		||||
		<svg viewBox="0 0 24 24">
 | 
			
		||||
			<path fill="#000000" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
 | 
			
		||||
		</svg>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="contented-upload-type-selector">
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-type contented-upload-type-active" data-upload-type="drag" title="Drag and drop">
 | 
			
		||||
			<svg viewBox="0 0 24 24">
 | 
			
		||||
				<path fill="#000000" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"></path>
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-type" data-upload-type="file" title="Multiple files">
 | 
			
		||||
			<svg viewBox="0 0 24 24">
 | 
			
		||||
				<path fill="#000000" d="M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z"></path>
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-type" data-upload-type="paste" title="Paste">
 | 
			
		||||
			<svg viewBox="0 0 24 24">
 | 
			
		||||
				<path fill="#000000" d="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z" />
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
		
 | 
			
		||||
		<div class="contented-upload-type" data-upload-type="drawing" title="Drawing">
 | 
			
		||||
			<svg viewBox="0 0 24 24">
 | 
			
		||||
				<path fill="#000000" d="M16.84,2.73C16.45,2.73 16.07,2.88 15.77,3.17L13.65,5.29L18.95,10.6L21.07,8.5C21.67,7.89 21.67,6.94 21.07,6.36L17.9,3.17C17.6,2.88 17.22,2.73 16.84,2.73M12.94,6L4.84,14.11L7.4,14.39L7.58,16.68L9.86,16.85L10.15,19.41L18.25,11.3M4.25,15.04L2.5,21.73L9.2,19.94L8.96,17.78L6.65,17.61L6.47,15.29" />
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-type" data-upload-type="album" title="Album">
 | 
			
		||||
			<svg viewBox="0 0 24 24">
 | 
			
		||||
				<path fill="#000000" d="M3,3H21V7H3V3M4,8H20V21H4V8M9.5,11A0.5,0.5 0 0,0 9,11.5V13H15V11.5A0.5,0.5 0 0,0 14.5,11H9.5Z" />
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	
 | 
			
		||||
	<div class="contented-content-area">
 | 
			
		||||
		<div class="contented-upload-if contented-if-drag contented-active">
 | 
			
		||||
			<label>Drop files or Ctrl-V to upload <span class="contented-extratext"></span></label>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-if contented-if-file">
 | 
			
		||||
			<label>Select files to upload <span class="contented-extratext"></span></label><br>
 | 
			
		||||
			<input class="contented-file-selector" type="file" multiple>
 | 
			
		||||
			<button class="contented-file-upload">Upload »</button>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-if contented-if-paste">
 | 
			
		||||
			<textarea placeholder="Paste content here"></textarea>
 | 
			
		||||
			<button class="contented-paste-upload">Upload »</button>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-if contented-if-drawing">
 | 
			
		||||
			<div class="contented-drawing-area"></div>
 | 
			
		||||
		</div>
 | 
			
		||||
		
 | 
			
		||||
		<div class="contented-upload-if contented-if-progress">
 | 
			
		||||
			<label>...</label>
 | 
			
		||||
			<div class="contented-progress-bar"><div class="contented-progress-element"></div></div>
 | 
			
		||||
		</div>
 | 
			
		||||
 | 
			
		||||
		<div class="contented-upload-if contented-if-album">
 | 
			
		||||
			<input type="text" class="contented-album-title" placeholder="Album name"><br>
 | 
			
		||||
			<input type="text" class="contented-album-items" placeholder="Image IDs, separated with commas">
 | 
			
		||||
			<br><br>
 | 
			
		||||
			<button class="contented-album-upload">Make album »</button>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
	var initArea = function (aboutInfo, elementSelector, onUploaded, onClose) {
 | 
			
		||||
		onUploaded = onUploaded || function () { };
 | 
			
		||||
		onClose = onClose || function () { };
 | 
			
		||||
 | 
			
		||||
@@ -153,15 +233,12 @@ var contented = (function() {
 | 
			
		||||
		// <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) + ")";
 | 
			
		||||
		if (aboutInfo.MaxUploadBytes > 0) {
 | 
			
		||||
			extraText = " (max " + formatBytes(aboutInfo.MaxUploadBytes) + ")";
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
				$.get(contented.baseURL + "widget.html", function (widgetHtml) {
 | 
			
		||||
 | 
			
		||||
		var $f = $("<div>").html(widgetHtml);
 | 
			
		||||
		$f.find(".contented-extratext").text(extraText);
 | 
			
		||||
 | 
			
		||||
@@ -243,6 +320,37 @@ var contented = (function() {
 | 
			
		||||
			handleUploadFrom([blob]);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Album
 | 
			
		||||
 | 
			
		||||
		$f.find(".contented-album-upload").on("click", function(e) {
 | 
			
		||||
			e.preventDefault();
 | 
			
		||||
			e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
			var title = $(".contented-album-title").val();
 | 
			
		||||
			if (title === "") {
 | 
			
		||||
				title = "Untitled album";
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var childIDs = $(".contented-album-items").val().split(",");
 | 
			
		||||
 | 
			
		||||
			for (var i = 0; i < childIDs.length; ++i) {
 | 
			
		||||
				childIDs[i] = childIDs[i].trim(); // Basic validation - can't really perform full validation here
 | 
			
		||||
 | 
			
		||||
				if (childIDs[i].length == 0) {
 | 
			
		||||
					alert("Entry " + (i+1) + " is too short, expected non-zero length");
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if (! childIDs[i].match(/^[a-zA-Z0-9]+$/)) {
 | 
			
		||||
					alert("Entry " + (i+1) + " contains unexpected character");
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			var blob = new Blob([JSON.stringify(childIDs)], {type : 'contented/album'});
 | 
			
		||||
			handleUploadFrom([new File([blob], title, {type: 'contented/album'})]);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// Ctrl+V uploads
 | 
			
		||||
		
 | 
			
		||||
		var pasteHandler = function(e) {
 | 
			
		||||
@@ -379,6 +487,10 @@ var contented = (function() {
 | 
			
		||||
 | 
			
		||||
		// Common upload handler
 | 
			
		||||
		
 | 
			
		||||
		/**
 | 
			
		||||
		 * 
 | 
			
		||||
		 * @param {File[]|Blob[]} files 
 | 
			
		||||
		 */
 | 
			
		||||
		var handleUploadFrom = function(files) {
 | 
			
		||||
 | 
			
		||||
			setProgressCaption("Uploading, please wait...");
 | 
			
		||||
@@ -432,33 +544,97 @@ var contented = (function() {
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
					// .
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
				});
 | 
			
		||||
	var init = function() {
 | 
			
		||||
 | 
			
		||||
			});
 | 
			
		||||
		var currentScriptPath = getCurrentScriptPath();
 | 
			
		||||
		var baseURL = currentScriptPath.replace('sdk.js', '');
 | 
			
		||||
		
 | 
			
		||||
		// Kick off background promises
 | 
			
		||||
 | 
			
		||||
		var loader = new Promise(function(resolve, reject) {
 | 
			
		||||
			if (typeof jQuery === "undefined") {
 | 
			
		||||
				loadScript(contented.baseURL + "jquery-3.7.0.min.js").then(resolve);
 | 
			
		||||
			} else {
 | 
			
		||||
				resolve();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		// Update fields in global variable
 | 
			
		||||
		contented.init = initArea;
 | 
			
		||||
		contented.loaded = true;
 | 
			
		||||
		}).then(function() { return new Promise(function(resolve, reject) {
 | 
			
		||||
			if (typeof DrawingBoard === "undefined") {
 | 
			
		||||
				loadScript(contented.baseURL + "drawingboard-0.4.6.min.js").then(resolve);
 | 
			
		||||
			} else {
 | 
			
		||||
				resolve();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
		// 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]);
 | 
			
		||||
		})}).then(function() { return new Promise(function(resolve, reject) {
 | 
			
		||||
			$.get(contented.baseURL + "about", function (aboutInfo) {
 | 
			
		||||
				resolve(aboutInfo);
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
		})}).then(function(aboutInfo) {
 | 
			
		||||
			// Update fields in global variable
 | 
			
		||||
			window.contented.loaded = true;
 | 
			
		||||
			
 | 
			
		||||
			return aboutInfo;
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		window.contented = {
 | 
			
		||||
			"loaded": false,
 | 
			
		||||
			
 | 
			
		||||
			"baseURL": baseURL,
 | 
			
		||||
			
 | 
			
		||||
			"__preInit": [],
 | 
			
		||||
			
 | 
			
		||||
			/**
 | 
			
		||||
			* initArea shows the contented upload widget over the top of a target DOM element.
 | 
			
		||||
			*
 | 
			
		||||
			* @param any        element       Drop target (string selector / DOMElement / jQuery)
 | 
			
		||||
			* @param Function   onUploaded    Called with an array of upload IDs
 | 
			
		||||
			* @param Function   onClose       Called when the widget is being destroyed
 | 
			
		||||
			*/
 | 
			
		||||
			"init": function(elementSelector, onUploaded, onClose) {
 | 
			
		||||
				loader.then(function(aboutInfo) {
 | 
			
		||||
					initArea(aboutInfo, elementSelector, onUploaded, onClose);
 | 
			
		||||
				});
 | 
			
		||||
			},
 | 
			
		||||
			
 | 
			
		||||
			/**
 | 
			
		||||
			* supportsDrop returns whether drag-and-drop is supported by this browser.
 | 
			
		||||
			*
 | 
			
		||||
			* @return bool
 | 
			
		||||
			*/
 | 
			
		||||
			"supportsDrop": function() {
 | 
			
		||||
				return ('ondrop' in window && 'FormData' in window && 'FileReader' in window);
 | 
			
		||||
			},
 | 
			
		||||
			
 | 
			
		||||
			"getPreviewURL": function(id) {
 | 
			
		||||
				return baseURL + "p/" + encodeURIComponent(id);
 | 
			
		||||
			},
 | 
			
		||||
			"getMultiPreviewURL": function(items) {
 | 
			
		||||
				return baseURL + "p/" + encodeURIComponent(items.join("-"));
 | 
			
		||||
			},
 | 
			
		||||
			"getDownloadURL": function(id) {
 | 
			
		||||
				return baseURL + "get/" + encodeURIComponent(id);
 | 
			
		||||
			},
 | 
			
		||||
			"getInfoJSONURL": function(id) {
 | 
			
		||||
				return baseURL + "info/" + encodeURIComponent(id);
 | 
			
		||||
			},
 | 
			
		||||
			"getThumbnailURL": function(thumbnailType, id) {
 | 
			
		||||
				return baseURL + "thumb/" + encodeURIComponent(thumbnailType) + "/" + encodeURIComponent(id);
 | 
			
		||||
			},
 | 
			
		||||
			"thumbnail": {
 | 
			
		||||
				"small_square": "s",
 | 
			
		||||
				"medium_square": "b",
 | 
			
		||||
				"medium": "t",
 | 
			
		||||
				"large": "m",
 | 
			
		||||
				"xlarge": "l",
 | 
			
		||||
				"xxlarge": "h"
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
	
 | 
			
		||||
	// Load scripts
 | 
			
		||||
	var needScripts = [];
 | 
			
		||||
	if (typeof jQuery === "undefined") {
 | 
			
		||||
		needScripts.push(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);
 | 
			
		||||
	init();
 | 
			
		||||
 | 
			
		||||
})()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,161 +0,0 @@
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
.contented {
 | 
			
		||||
    box-sizing:border-box;
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    border: 8px dashed lightgrey;
 | 
			
		||||
    padding: 12px;
 | 
			
		||||
    background:white; /* not transparent */
 | 
			
		||||
 | 
			
		||||
    text-overflow:hidden;
 | 
			
		||||
    overflow:auto;
 | 
			
		||||
    width:100%;
 | 
			
		||||
    height:100%;
 | 
			
		||||
 | 
			
		||||
    position:relative;
 | 
			
		||||
}
 | 
			
		||||
.contented .contented-close {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 12px;
 | 
			
		||||
    right: 12px;
 | 
			
		||||
    width: 24px;
 | 
			
		||||
    height: 24px;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
.contented .contented-upload-type-selector {
 | 
			
		||||
    display:block;
 | 
			
		||||
    margin-bottom: 1em;
 | 
			
		||||
    -webkit-user-select: none;
 | 
			
		||||
    user-select: none; 
 | 
			
		||||
}
 | 
			
		||||
.contented .contented-upload-type {
 | 
			
		||||
    display:inline-block;
 | 
			
		||||
    opacity:0.2;
 | 
			
		||||
    transition:opacity linear 0.1s;
 | 
			
		||||
    cursor:pointer;
 | 
			
		||||
}
 | 
			
		||||
.contented .contented-upload-type:hover {
 | 
			
		||||
    opacity:0.5;
 | 
			
		||||
    transition:opacity linear 0s;
 | 
			
		||||
}
 | 
			
		||||
.contented .contented-upload-type svg {
 | 
			
		||||
    width:36px;
 | 
			
		||||
    height:36px;
 | 
			
		||||
}
 | 
			
		||||
.contented .contented-upload-type.contented-upload-type-active {
 | 
			
		||||
    opacity:1;
 | 
			
		||||
}
 | 
			
		||||
.contented.is-dragging {
 | 
			
		||||
    background: lightblue;
 | 
			
		||||
}
 | 
			
		||||
.contented-content-area {
 | 
			
		||||
    position:absolute;
 | 
			
		||||
    top: 60px;
 | 
			
		||||
    bottom: 10px;
 | 
			
		||||
    left: 10px;
 | 
			
		||||
    width: calc(100% - 20px);
 | 
			
		||||
 | 
			
		||||
    /* Prevent blur under translateY */
 | 
			
		||||
    -webkit-transform-style: preserve-3d;
 | 
			
		||||
    -moz-transform-style: preserve-3d;
 | 
			
		||||
    transform-style: preserve-3d;
 | 
			
		||||
}
 | 
			
		||||
.contented-content-area > div {
 | 
			
		||||
    position: relative;
 | 
			
		||||
    top: 50%;
 | 
			
		||||
    transform: translateY(-50%);
 | 
			
		||||
}
 | 
			
		||||
.contented-upload-if {
 | 
			
		||||
    display:none;
 | 
			
		||||
}
 | 
			
		||||
.contented-if-paste, .contented-if-drawing {
 | 
			
		||||
    height:100%;
 | 
			
		||||
}
 | 
			
		||||
.contented-upload-if.contented-active {
 | 
			
		||||
    display:block;
 | 
			
		||||
}
 | 
			
		||||
.contented textarea {
 | 
			
		||||
    resize: none;
 | 
			
		||||
    width:100%;
 | 
			
		||||
    height:calc(100% - 1em - 15px);
 | 
			
		||||
    box-sizing:border-box;
 | 
			
		||||
}
 | 
			
		||||
.contented-progress-bar {
 | 
			
		||||
    display: block;
 | 
			
		||||
    width:90%;
 | 
			
		||||
    margin:0.5em auto 0 auto;
 | 
			
		||||
 | 
			
		||||
    height:16px;
 | 
			
		||||
    border-radius:8px;
 | 
			
		||||
    background:lightgrey;
 | 
			
		||||
    position:relative;
 | 
			
		||||
    overflow:hidden;
 | 
			
		||||
}
 | 
			
		||||
.contented-progress-element {
 | 
			
		||||
    position:absolute;
 | 
			
		||||
    background:darkgreen;
 | 
			
		||||
    left:0;
 | 
			
		||||
    width:0%;
 | 
			
		||||
    height:100%;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
<div class="contented">
 | 
			
		||||
 | 
			
		||||
    <div class="contented-close">
 | 
			
		||||
        <svg viewBox="0 0 24 24">
 | 
			
		||||
            <path fill="#000000" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
 | 
			
		||||
        </svg>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="contented-upload-type-selector">
 | 
			
		||||
 | 
			
		||||
        <div class="contented-upload-type contented-upload-type-active" data-upload-type="drag" title="Drag and drop">
 | 
			
		||||
            <svg viewBox="0 0 24 24">
 | 
			
		||||
                <path fill="#000000" d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="contented-upload-type" data-upload-type="file" title="Multiple files">
 | 
			
		||||
            <svg viewBox="0 0 24 24">
 | 
			
		||||
                <path fill="#000000" d="M15,7H20.5L15,1.5V7M8,0H16L22,6V18A2,2 0 0,1 20,20H8C6.89,20 6,19.1 6,18V2A2,2 0 0,1 8,0M4,4V22H20V24H4A2,2 0 0,1 2,22V4H4Z"></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="contented-upload-type" data-upload-type="paste" title="Paste">
 | 
			
		||||
            <svg viewBox="0 0 24 24">
 | 
			
		||||
                <path fill="#000000" d="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z" />
 | 
			
		||||
            </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="contented-upload-type" data-upload-type="drawing" title="Drawing">
 | 
			
		||||
			<svg viewBox="0 0 24 24">
 | 
			
		||||
				<path fill="#000000" d="M16.84,2.73C16.45,2.73 16.07,2.88 15.77,3.17L13.65,5.29L18.95,10.6L21.07,8.5C21.67,7.89 21.67,6.94 21.07,6.36L17.9,3.17C17.6,2.88 17.22,2.73 16.84,2.73M12.94,6L4.84,14.11L7.4,14.39L7.58,16.68L9.86,16.85L10.15,19.41L18.25,11.3M4.25,15.04L2.5,21.73L9.2,19.94L8.96,17.78L6.65,17.61L6.47,15.29" />
 | 
			
		||||
			</svg>
 | 
			
		||||
		</div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div class="contented-content-area">
 | 
			
		||||
        <div class="contented-upload-if contented-if-drag contented-active">
 | 
			
		||||
            <label>Drop files or Ctrl-V to upload <span class="contented-extratext"></span></label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="contented-upload-if contented-if-file">
 | 
			
		||||
            <label>Select files to upload <span class="contented-extratext"></span></label><br>
 | 
			
		||||
            <input class="contented-file-selector" type="file" multiple>
 | 
			
		||||
            <button class="contented-file-upload">Upload »</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="contented-upload-if contented-if-paste">
 | 
			
		||||
            <textarea placeholder="Paste content here"></textarea>
 | 
			
		||||
            <button class="contented-paste-upload">Upload »</button>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="contented-upload-if contented-if-drawing">
 | 
			
		||||
			<div class="contented-drawing-area"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="contented-upload-if contented-if-progress">
 | 
			
		||||
            <label>...</label>
 | 
			
		||||
            <div class="contented-progress-bar"><div class="contented-progress-element"></div></div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										57
									
								
								storage.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								storage.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,57 @@
 | 
			
		||||
package contented
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"github.com/minio/minio-go/v7"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (this *Server) ReadFile(ctx context.Context, fileHash string) (io.ReadSeekCloser, error) {
 | 
			
		||||
	if this.opts.StorageType == STORAGE_LOCAL {
 | 
			
		||||
		fh, err := os.Open(filepath.Join(this.opts.DataDirectory, fileHash))
 | 
			
		||||
		return fh, err
 | 
			
		||||
 | 
			
		||||
	} else if this.opts.StorageType == STORAGE_S3 {
 | 
			
		||||
		obj, err := this.s3client.GetObject(ctx, this.opts.DataS3Options.Bucket, this.opts.DataS3Options.Prefix+fileHash, minio.GetObjectOptions{})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return obj, nil
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		panic("bad StorageType")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (this *Server) SaveFile(ctx context.Context, fileHash string, srcLen int64, src io.Reader) error {
 | 
			
		||||
	if this.opts.StorageType == STORAGE_LOCAL {
 | 
			
		||||
 | 
			
		||||
		// Save file to disk
 | 
			
		||||
		dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, this.opts.FileMode())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			if os.IsExist(err) {
 | 
			
		||||
				return nil // hash matches existing upload
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return err // Real error
 | 
			
		||||
		}
 | 
			
		||||
		defer dest.Close()
 | 
			
		||||
 | 
			
		||||
		_, err = io.CopyN(dest, src, int64(srcLen))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
 | 
			
		||||
	} else if this.opts.StorageType == STORAGE_S3 {
 | 
			
		||||
		_, err := this.s3client.PutObject(ctx, this.opts.DataS3Options.Bucket, this.opts.DataS3Options.Prefix+fileHash, src, srcLen, minio.PutObjectOptions{})
 | 
			
		||||
		return err
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		panic("bad StorageType")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										44
									
								
								thumb.go
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								thumb.go
									
									
									
									
									
								
							@@ -4,8 +4,10 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"code.ivysaur.me/thumbnail"
 | 
			
		||||
@@ -62,7 +64,12 @@ func (this *Server) handleThumb(w http.ResponseWriter, r *http.Request, thumbnai
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Only a limited number of thumbnails can be generated concurrently
 | 
			
		||||
	<-this.thumbnailSem
 | 
			
		||||
	select {
 | 
			
		||||
	case <-this.thumbnailSem:
 | 
			
		||||
	case <-r.Context().Done():
 | 
			
		||||
		http.Error(w, r.Context().Err().Error(), 400) // probably won't be delivered anyway
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer func() { this.thumbnailSem <- struct{}{} }()
 | 
			
		||||
 | 
			
		||||
	if ctx.Err() != nil {
 | 
			
		||||
@@ -89,12 +96,45 @@ func (this *Server) handleThumbInternal(ctx context.Context, w http.ResponseWrit
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	filePath := filepath.Join(this.opts.DataDirectory, m.FileHash)
 | 
			
		||||
	if m.FileSize > this.opts.MaxThumbSizeBytes {
 | 
			
		||||
		return errors.New("Don't want to thumbnail very large files, sorry")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var filePath string
 | 
			
		||||
 | 
			
		||||
	if this.opts.StorageType == STORAGE_LOCAL {
 | 
			
		||||
		filePath = filepath.Join(this.opts.DataDirectory, m.FileHash)
 | 
			
		||||
 | 
			
		||||
	} else if this.opts.StorageType == STORAGE_S3 {
 | 
			
		||||
		// Need to temporarily download it for thumbnailing (slow and costs money)
 | 
			
		||||
 | 
			
		||||
		destFh, err := os.CreateTemp("", "contented-thumbcache-*")
 | 
			
		||||
		defer os.Remove(destFh.Name())
 | 
			
		||||
 | 
			
		||||
		srcFh, err := this.ReadFile(ctx, m.FileHash)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = io.CopyN(destFh, srcFh, m.FileSize)
 | 
			
		||||
		srcFh.Close()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		destFh.Seek(0, io.SeekStart)
 | 
			
		||||
		filePath = destFh.Name()
 | 
			
		||||
 | 
			
		||||
	} else {
 | 
			
		||||
		panic("bad StorageType")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	thumb, err := t.RenderFileAs(filePath, m.MimeType)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w.Header().Set(`Cache-Control`, `max-age=31536000, immutable`)
 | 
			
		||||
	w.Header().Set(`Content-Length`, fmt.Sprintf("%d", len(thumb)))
 | 
			
		||||
	w.Header().Set(`Content-Type`, `image/jpeg`)
 | 
			
		||||
	w.WriteHeader(200)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										25
									
								
								upload.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								upload.go
									
									
									
									
									
								
							@@ -1,6 +1,7 @@
 | 
			
		||||
package contented
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha512"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
@@ -9,15 +10,18 @@ import (
 | 
			
		||||
	"mime"
 | 
			
		||||
	"mime/multipart"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
 | 
			
		||||
 | 
			
		||||
	if !this.opts.EnableUpload {
 | 
			
		||||
		http.Error(w, "Server is read-only", 403)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	remoteIP := this.remoteIP(r)
 | 
			
		||||
 | 
			
		||||
	err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory
 | 
			
		||||
@@ -89,26 +93,13 @@ func (this *Server) handleUploadFile(src multipart.File, hdr *multipart.FileHead
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Save file to disk
 | 
			
		||||
	fileHash := hex.EncodeToString(hasher.Sum(nil))
 | 
			
		||||
	dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, this.opts.FileMode())
 | 
			
		||||
	shouldSave := true
 | 
			
		||||
	if err != nil && os.IsExist(err) {
 | 
			
		||||
		// hash matches existing upload
 | 
			
		||||
		// That's fine - but still persist the metadata separately
 | 
			
		||||
		shouldSave = false
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if shouldSave {
 | 
			
		||||
		defer dest.Close()
 | 
			
		||||
 | 
			
		||||
		_, err = io.CopyN(dest, src, int64(srcLen))
 | 
			
		||||
	// Save file to disk/s3
 | 
			
		||||
	err = this.SaveFile(context.Background(), fileHash, srcLen, src)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Determine mime type
 | 
			
		||||
	ctype := hdr.Header.Get("Content-Type")
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user