73 Commits

Author SHA1 Message Date
ccc1f73e52 doc: update README for 1.3.1 2020-07-25 13:17:02 +12:00
b48d1347c7 vendor: update package versions (fixes FTBFS) 2020-07-25 13:15:50 +12:00
bed8a88b09 vendor: delete vendor directory, rely fully on Go modules 2020-07-25 13:14:44 +12:00
0a70d99af5 changelog for 1.3.0 2020-07-25 12:35:26 +12:00
6720cbc0d9 upload: buffer MIME parsing on disk, not in memory 2020-07-25 12:25:44 +12:00
8d11b7c434 doc/README: add links for archived releases 2020-05-06 18:05:58 +12:00
147608a327 hg2git: fix source tarball generation in Makefile 2018-12-31 19:02:58 +13:00
951c87b63f convert to Go Modules 2018-12-31 19:02:50 +13:00
25180afaa2 doc: fix markdown syntax 2018-10-06 14:04:13 +13:00
33ca03b9d7 doc: update README for GFM syntax 2018-10-06 13:49:20 +13:00
b562ca4bd4 doc: move marketing images to /doc/ subdir 2018-10-06 13:49:00 +13:00
a25dc5827b doc: add image1.png thumbnail 2018-10-06 13:43:02 +13:00
5263f96956 doc: update readme 2018-09-09 19:03:29 +12:00
2b2add62b2 vcs: remove old hgtags 2018-09-09 18:45:14 +12:00
fdc93e6485 vcs: migrate hgignore->gitignore 2018-09-09 18:45:07 +12:00
b08f1c33d5 thumbs: allow configuring limit on simultanous thumbs (default 16) 2018-09-09 18:41:37 +12:00
524f37d9fe serve text/plain content with charset=utf-8 header 2018-09-09 18:33:33 +12:00
c958c57794 thumb: only generate one thumbnail concurrently 2018-09-09 18:31:25 +12:00
8fbad2a1e0 doc: move README to top-level, for web viewing 2018-07-21 13:38:46 +12:00
989cd195f8 doc: remove TODO file
Issues are now tracked at git.ivysaur.me.
2018-07-21 13:36:14 +12:00
cb4933fa72 doc: update TODO 2018-07-10 17:45:58 +12:00
f504ab5929 bump version to 1.2.2 2018-06-09 18:13:22 +12:00
f8e95a8037 add go-get tags to readme 2018-06-09 18:12:50 +12:00
feaa51cfcd Added tag v1.2.1 for changeset 7c3807929e7a 2018-06-09 18:11:39 +12:00
ddf76aacc5 doc: update readme 2018-06-09 18:10:54 +12:00
ca8a0d55ba compatibility fixes for hashids library 2018-06-09 18:10:49 +12:00
fe4aace777 thumb: compatibility fixes for thumbnail library 2018-06-09 18:09:57 +12:00
be864235cb vendor: update thumbnail from 1.0.0 -> master 2018-06-09 18:09:49 +12:00
dba699524f build: remove disableimagecrush tag, as it's now the default 2018-06-09 17:55:25 +12:00
7950ca3004 use dep to manage vendor directory 2018-06-09 17:54:49 +12:00
6e5ca0c61c retag releases to semver format 2018-06-04 18:30:42 +12:00
7255cd03cb makefile: build without imagecrush support 2018-06-04 18:28:58 +12:00
3cf4986418 fix typo in previous 2018-06-04 18:28:50 +12:00
0a338c4568 add OpenGraph meta tags, for image preview inside chat apps like Telegram 2018-06-04 17:22:28 +12:00
0fbf401fd3 doc: update TODO 2018-03-01 18:38:42 +13:00
48ca68fafd Added tag release-1.2.0 for changeset 0f021da52854 2017-11-18 14:30:26 +13:00
f3e594d307 bump all versions to 1.2.1 2017-11-18 14:30:21 +13:00
396672b02b 1.2.0 release makefile 2017-11-18 14:30:13 +13:00
d25b867e90 doc: changelog 2017-11-18 14:29:02 +13:00
cd60e4c855 add diskFilesWorldReadable option to control 0644/0600 choice for new files 2017-11-18 14:15:31 +13:00
ee72f188a2 doc: update TODO 2017-11-18 14:12:55 +13:00
30f5b40e1d staticResources: rebuild 2017-11-18 14:11:44 +13:00
3a103ae484 index: replace our custom result listing page with an album preview 2017-11-18 14:11:40 +13:00
08321818ff sdk: add getMultiPreviewURL() function 2017-11-18 14:11:27 +13:00
14456e6539 preview: support multi-image albums 2017-11-18 14:11:20 +13:00
f27d14aac4 preview: add page title, viewport, add 'again...' button 2017-11-18 13:53:46 +13:00
6ab2b08099 sdk: getPreviewURL() now uses the real preview page 2017-11-18 13:53:33 +13:00
930869759b add a preview page 2017-11-18 13:48:34 +13:00
d35c81ed21 doc: preliminary changelog update 2017-11-18 13:34:49 +13:00
139117b4d5 staticResources: rebuild 2017-11-18 13:33:15 +13:00
b88273ec64 index: display thumbnail after upload 2017-11-18 13:32:54 +13:00
79cb8733e5 sdk: add encodeURIComponent() to getters, add getThumbnailURL(), add thumbnail.* constants 2017-11-18 13:32:47 +13:00
366c307e02 staticResources: rebuild 2017-11-18 13:27:11 +13:00
23ad509f33 thumbnailer: display a static error image on failure 2017-11-18 13:27:07 +13:00
a6e495f74d initial thumbnailing support 2017-11-18 13:11:39 +13:00
b3ec40ae65 doc: update TODO 2017-11-16 19:53:37 +13:00
87f0cb016d bump version to 1.1.1 2017-10-15 22:04:30 +13:00
381e67bb39 Added tag release-1.1.0 for changeset 98da2ebf0d50 2017-10-15 22:04:16 +13:00
c2f4de822f Removed tag release-1.1.0 2017-10-15 22:03:52 +13:00
ffd4c03d9c Backed out changeset: fa32e83c5a38 2017-10-15 22:03:47 +13:00
2e08ac06ca rebuild staticResources.go 2017-10-15 22:02:41 +13:00
6d739972de fix border radius display for progress bar 2017-10-15 22:02:31 +13:00
e36fd43f9b fix missing green part of progress bar 2017-10-15 22:02:19 +13:00
35bbc6c61b fix loading drawingboard css from relative URL 2017-10-15 22:02:08 +13:00
987c704730 load dependent scripts sequentially 2017-10-15 21:58:22 +13:00
9669f2aa0b bump all versions to 1.1.1 2017-10-15 20:54:41 +13:00
99139e360d Added tag release-1.1.0 for changeset cfb1e028fd06 2017-10-15 20:54:29 +13:00
696a92096d Backed out changeset: 77530eea6f02 2017-10-15 20:53:59 +13:00
f125c23fb9 Removed tag release-1.1.0 2017-10-15 20:53:35 +13:00
c50d6c4bfe rebuild staticResources.go for previous 2017-10-15 20:53:06 +13:00
5edea74333 fix missing baseURLs on remote script loads 2017-10-15 20:49:07 +13:00
1882a94e65 Added tag release-1.1.0 for changeset c7b699105bd1 2017-10-15 19:52:15 +13:00
1309293705 bump all versions to 1.1.1 2017-10-15 19:52:09 +13:00
26 changed files with 606 additions and 161 deletions

View File

@@ -1,5 +1,3 @@
syntax: glob
cmd/contented/contented cmd/contented/contented
build/ build/
_dist/ _dist/

View File

@@ -1,2 +0,0 @@
e2250a7fd29052ea767f18e1459cabea4cd7efd3 release-1.0.0
b8975b9e75648a7c2a5003c67db92cf2216e01c0 release-1.0.1

View File

@@ -2,7 +2,7 @@
# Makefile for contented # Makefile for contented
# #
VERSION:=1.1.0 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

View File

@@ -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
View File

@@ -0,0 +1,128 @@
# contented
[![](doc/image1.thumb.png)](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)*

View File

@@ -5,6 +5,8 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"os"
"regexp"
"strings" "strings"
"time" "time"
@@ -14,24 +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
TrustXForwardedFor bool BandwidthLimit int64
EnableHomepage bool 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
} }
@@ -42,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
@@ -90,8 +117,11 @@ func (this *Server) remoteIP(r *http.Request) string {
const ( const (
downloadUrlPrefix = `/get/` downloadUrlPrefix = `/get/`
metadataUrlPrefix = `/info/` metadataUrlPrefix = `/info/`
previewUrlPrefix = `/p/`
) )
var rxThumbUrl = regexp.MustCompile(`^/thumb/(.)/(.*)$`)
func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(`Server`, SERVER_HEADER) w.Header().Set(`Server`, SERVER_HEADER)
w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS
@@ -110,6 +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)

View File

@@ -1,13 +0,0 @@
TODO
- View server-wide recent uploads history / all upload history
- Display 'my uploads' (id + metadata history kept in localstorage)
- Encrypted at rest (anti- provider snooping)
- Nicer preview page after uploading
- Gallery preview (multiple element hashid)
- Prevent selecting around the toggle buttons (firefox for android)

View File

@@ -1,82 +0,0 @@
A file / image / paste upload server with a focus on embedding.
Written in Go
You can use contented as a standalone upload server, or you can use the SDK to embed its upload widget into another website.
=FEATURES=
- Drag and drop upload
- Multiple files upload
- Pastebin upload
- Custom drawing upload ([url=https://github.com/Leimi/drawingboard.js]via[/url])
- Ctrl-V upload
- SDK-oriented design for embedding, including CORS support
- Mobile friendly HTML interface
- Preserves uploaded filename and content-type metadata
- Hash verification (SHA512/256)
- Detect duplicate upload content and reuse storage
- Options to limit the upload filesize and the upload bandwidth
- Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm)
=USAGE (SERVER)=
`Usage of contented:
-data string
Directory for stored content (default "")
-db string
Path for metadata database (default "contented.db")
-enableHomepage
Enable homepage (disable for embedded use only) (default true)
-listen string
(default "127.0.0.1:80")
-max int
Maximum size of uploaded files in MiB (set zero for unlimited) (default 8)
-speed int
Maximum upload speed in bytes/sec (set zero for unlimited)
-title string
Title used in web interface (default "contented")
-trustXForwardedFor
Trust X-Forwarded-For reverse proxy headers
`
If you are hosting behind a reverse proxy, remember to set its post body size parameter appropriately (e.g. `client_max_body_size` for nginx).
=USAGE (HTTP)=
The server responds on the following URLs:
- `/get/{ID}`: Download item content
- `/info/{ID}`: Get item content metadata (JSON)
- `/about`: Get server metadata (JSON)
=USAGE (EMBEDDING FOR WEB)=
Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element. Your callback will be passed an array of file IDs of any uploaded items.
`<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
contented.init("#target", function(/* String[] */ items) {});
`
=CHANGELOG=
2017-10-15: 1.1.0
- Feature: Drawing mode
- Feature: Ctrl+V image upload
- Feature: Option to trust X-Forwarded-For headers when using a reverse proxy
- Feature: Add `getDownloadURL`, `getInfoJSONURL`, `getPreviewURL` SDK methods
- Feature: Option to disable uploading via the homepage
- Feature: Add button to repeat when uploading from homepage
- Enhancement: Automatically load library dependencies
- Enhancement: Display homepage widget using the full screen size
- Include drawingboard.js 0.4.6 (MIT license)
- Fix a cosmetic issue with javascript console output
- Fix a cosmetic issue with error messages if an upload failed
2017-10-08: 1.0.1
- Fix an issue with CORS preflight requests
- Fix an issue with index URLs
2017-10-08: 1.0.0
- Initial public release
- Include jQuery 1.12.4 (MIT license)

View File

@@ -20,15 +20,19 @@ func main() {
maxUploadSpeed := flag.Int("speed", 0, "Maximum upload speed in bytes/sec (set zero for unlimited)") maxUploadSpeed := flag.Int("speed", 0, "Maximum upload speed in bytes/sec (set zero for unlimited)")
trustXForwardedFor := flag.Bool("trustXForwardedFor", false, "Trust X-Forwarded-For reverse proxy headers") trustXForwardedFor := flag.Bool("trustXForwardedFor", false, "Trust X-Forwarded-For reverse proxy headers")
enableHomepage := flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)") enableHomepage := flag.Bool("enableHomepage", true, "Enable homepage (disable for embedded use only)")
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, TrustXForwardedFor: *trustXForwardedFor,
EnableHomepage: *enableHomepage, 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,

View File

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
doc/image1.thumb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -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
View 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
View 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
View 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))
}

View File

@@ -56,22 +56,7 @@ $.get("/about", function(ret) {
// Load upload widget // Load upload widget
contented.init("#surrogate-area", function(items) { contented.init("#surrogate-area", function(items) {
window.location.href = contented.getMultiPreviewURL(items);
var $table = $("<table>");
for (var i = 0; i < items.length; ++i) {
$table.append($("<tr>").append([
$("<td>").text(items[i]),
$("<td>").html("<a target='_blank' href='" + contented.getDownloadURL(items[i]) + "'>get</a>"),
$("<td>").html("<a target='_blank' href='" + contented.getInfoJSONURL(items[i]) + "'>info</a>"),
]))
}
$("#surrogate-area").html([
$table,
$("<button>").addClass("again").text("Again...").click(function() {
window.location.href = window.location.href;
}),
]);
}); });
</script> </script>
</body> </body>

BIN
static/nothumb_1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
static/nothumb_160.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
static/nothumb_340.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
static/nothumb_640.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

BIN
static/nothumb_90.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -65,13 +65,27 @@ var contented = (function() {
}, },
"getPreviewURL": function(id) { "getPreviewURL": function(id) {
return baseURL + "get/" + id; // n.b. there's no better preview URL yet return baseURL + "p/" + encodeURIComponent(id);
},
"getMultiPreviewURL": function(items) {
return baseURL + "p/" + encodeURIComponent(items.join("-"));
}, },
"getDownloadURL": function(id) { "getDownloadURL": function(id) {
return baseURL + "get/" + id; return baseURL + "get/" + encodeURIComponent(id);
}, },
"getInfoJSONURL": function(id) { "getInfoJSONURL": function(id) {
return baseURL + "info/" + 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"
} }
}; };
@@ -88,21 +102,19 @@ var contented = (function() {
}; };
var loadScripts = function(urls, onLoad) { var loadScripts = function(urls, onLoad) {
if (urls.length === 0) { // load sequentially
onLoad(); var i = 0;
return; var loadNext = function() {
} if (i === urls.length) {
var totalLoaded = 0;
var cb = function() {
totalLoaded += 1;
if (totalLoaded == urls.length) {
onLoad(); onLoad();
return;
} }
var url = urls[i];
i += 1;
loadScript(url, loadNext);
}; };
for (var i = 0; i < urls.length; ++i) { loadNext();
loadScript(urls[i], cb);
}
}; };
var formatBytes = function(bytes) { var formatBytes = function(bytes) {
@@ -294,6 +306,10 @@ var contented = (function() {
var setupDrawingBoard = function() { var setupDrawingBoard = function() {
$("head").append(
'<link rel="stylesheet" type="text/css" href="' + contented.baseURL + 'drawingboard-0.4.6.min.css">'
);
var db_id = "contented-drawing-area-" + guid(); var db_id = "contented-drawing-area-" + guid();
var $db = $("<div>").attr('id', db_id); var $db = $("<div>").attr('id', db_id);
@@ -437,10 +453,10 @@ var contented = (function() {
// Load scripts // Load scripts
var needScripts = []; var needScripts = [];
if (typeof jQuery === "undefined") { if (typeof jQuery === "undefined") {
needScripts.push("jquery-1.12.4.min.js"); needScripts.push(contented.baseURL + "jquery-1.12.4.min.js");
} }
if (typeof DrawingBoard === "undefined") { if (typeof DrawingBoard === "undefined") {
needScripts.push("drawingboard-0.4.6.min.js"); needScripts.push(contented.baseURL + "drawingboard-0.4.6.min.js");
} }
loadScripts(needScripts, afterScriptsLoaded); loadScripts(needScripts, afterScriptsLoaded);

View File

@@ -88,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">
@@ -148,7 +150,6 @@
</div> </div>
<div class="contented-upload-if contented-if-drawing"> <div class="contented-upload-if contented-if-drawing">
<link rel="stylesheet" type="text/css" href="/drawingboard-0.4.6.min.css">
<div class="contented-drawing-area"></div> <div class="contented-drawing-area"></div>
</div> </div>

File diff suppressed because one or more lines are too long

104
thumb.go Normal file
View 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
}

View File

@@ -20,7 +20,7 @@ func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
remoteIP := this.remoteIP(r) remoteIP := this.remoteIP(r)
err := r.ParseMultipartForm(this.opts.MaxUploadBytes * 2) err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory
if err != nil { if err != nil {
log.Printf("%s Invalid request: %s\n", remoteIP, err.Error()) log.Printf("%s Invalid request: %s\n", remoteIP, err.Error())
http.Error(w, "Invalid request", 400) http.Error(w, "Invalid request", 400)
@@ -91,7 +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