104 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
d24c3d4895 doc: changelog 2017-10-15 19:51:36 +13:00
5e9fcc09c8 bump versions to 1.1.0 2017-10-15 19:51:17 +13:00
fb7f1dd3a5 doc: update changelog 2017-10-15 19:50:48 +13:00
2f4fe0a55f enable ctrl-V support 2017-10-15 19:49:46 +13:00
a3fc9092e3 doc: no need to point out the jquery dependency 2017-10-15 19:47:35 +13:00
6eb48bf72f doc: update changelog 2017-10-15 19:16:26 +13:00
40aa6c8917 option to disable the homepage 2017-10-15 19:16:22 +13:00
24febefba4 doc: update changelog 2017-10-15 19:12:25 +13:00
57d9b4d324 display xff IPs in log output 2017-10-15 19:11:13 +13:00
3ca73e3221 option for trustXForwardedFor 2017-10-15 19:09:14 +13:00
27cf4cf0c0 fix content selection in chrome prior to 54, et al 2017-10-15 19:00:49 +13:00
ef6b680b7e doc: update changelog 2017-10-15 18:59:00 +13:00
188ca7e679 homepage: add "again" button 2017-10-15 18:54:14 +13:00
b8f1b26aba sdk auto provide libraries, handle preInit calls, add get*URL methods, drawing canvas integration 2017-10-15 18:50:55 +13:00
ac768524ee homepage: display widget in full size 2017-10-15 18:49:46 +13:00
a5008ab455 vendor: track drawingboard 0.4.6 (MIT license) 2017-10-15 18:49:09 +13:00
9436dcd43e tweak progress captions in error cases 2017-10-08 19:14:11 +13:00
f543347b1c remove a console.log call 2017-10-08 19:07:06 +13:00
582854878b doc: mention client body size for nginx 2017-10-08 19:06:42 +13:00
642ee5a485 doc: todo (2) 2017-10-08 18:42:47 +13:00
19b20daded doc: todo 2017-10-08 18:40:43 +13:00
270e6a9397 bump all versions to 1.0.2 2017-10-08 17:16:37 +13:00
ef5351a075 Added tag release-1.0.1 for changeset b8975b9e7564 2017-10-08 17:16:27 +13:00
01ac021c7e doc: todo 2017-10-08 17:16:21 +13:00
47aeaff66e doc: changelog 2017-10-08 17:12:56 +13:00
d18a5fd272 bump all versions to 1.0.1 2017-10-08 17:12:49 +13:00
3a0566ca0a doc: tweak SDK information 2017-10-08 17:12:26 +13:00
36f62dd502 doc: add image 2017-10-08 17:12:17 +13:00
5b1a94c735 fix CORS 2017-10-08 17:06:24 +13:00
75c3a98f33 fix no default index page 2017-10-08 16:54:26 +13:00
e9fbfd0277 Added tag release-1.0.0 for changeset e2250a7fd290 2017-10-08 16:43:50 +13:00
27 changed files with 1133 additions and 333 deletions

View File

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

View File

@@ -2,7 +2,7 @@
# Makefile for contented
#
VERSION:=1.0.0
VERSION:=1.2.2
SOURCES:=Makefile \
static \
@@ -69,5 +69,5 @@ _dist/contented-$(VERSION)-win32.7z: build/win32/contented.exe
)
_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.Salt = hashIdSalt
hd.MinLength = hashIdMinLength
h, _ := hashids.NewWithData(hd)
h := hashids.NewWithData(hd)
s, _ := h.EncodeInt64([]int64{int64(v)})
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"
"log"
"net/http"
"os"
"regexp"
"strings"
"time"
@@ -14,22 +16,38 @@ import (
var SERVER_HEADER string = `contented/0.0.0-dev`
const DEFAULT_MAX_CONCURRENT_THUMBS = 16
type ServerPublicProperties struct {
AppTitle string
MaxUploadBytes int64
AppTitle string
MaxUploadBytes int64
CanonicalBaseURL string
}
type ServerOptions struct {
DataDirectory string
DBPath string
BandwidthLimit int64
DataDirectory string
DBPath string
DiskFilesWorldReadable bool
BandwidthLimit int64
TrustXForwardedFor bool
EnableHomepage bool
MaxConcurrentThumbs int
ServerPublicProperties
}
func (this *ServerOptions) FileMode() os.FileMode {
if this.DiskFilesWorldReadable {
return 0644
} else {
return 0600
}
}
type Server struct {
opts ServerOptions
db *bolt.DB
startTime time.Time
thumbnailSem chan struct{}
metadataBucket []byte
}
@@ -40,6 +58,17 @@ func NewServer(opts *ServerOptions) (*Server, error) {
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)
if err != nil {
return nil, err
@@ -75,17 +104,27 @@ func (this *Server) handleAbout(w http.ResponseWriter) {
this.serveJsonObject(w, this.opts.ServerPublicProperties)
}
func remoteIP(r *http.Request) string {
func (this *Server) remoteIP(r *http.Request) string {
if this.opts.TrustXForwardedFor {
if xff := r.Header.Get("X-Forwarded-For"); len(xff) > 0 {
return xff
}
}
return strings.TrimRight(strings.TrimRight(r.RemoteAddr, "0123456789"), ":")
}
const (
downloadUrlPrefix = `/get/`
metadataUrlPrefix = `/info/`
previewUrlPrefix = `/p/`
)
var rxThumbUrl = regexp.MustCompile(`^/thumb/(.)/(.*)$`)
func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set(`Server`, SERVER_HEADER)
w.Header().Set(`Access-Control-Allow-Origin`, `*`) // Blanket allow CORS
if this.opts.MaxUploadBytes > 0 {
r.Body = http.MaxBytesReader(w, r.Body, this.opts.MaxUploadBytes)
}
@@ -101,6 +140,13 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} else if r.Method == "GET" && strings.HasPrefix(r.URL.Path, 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` {
this.handleAbout(w)
@@ -108,11 +154,13 @@ func (this *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
this.handleUpload(w, r)
} else if r.Method == "OPTIONS" {
// Blanket allow
w.Header().Set(`Access-Control-Allow-Origin`, `*`)
// Blanket allow (headers already set)
w.WriteHeader(200)
} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" {
} else if r.Method == "GET" && r.URL.Path == `/` && this.opts.EnableHomepage {
http.Redirect(w, r, `/index.html`, http.StatusFound)
} else if static, err := Asset(r.URL.Path[1:]); err == nil && r.Method == "GET" && (this.opts.EnableHomepage || r.URL.Path != `/index.html`) {
http.ServeContent(w, r, r.URL.Path[1:], this.startTime, bytes.NewReader(static))
} else {

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
- Option to disable manual uploading from the landing page
- Gallery preview (multiple element hashid)

View File

@@ -1,61 +0,0 @@
A file / image / paste upload server with a focus on embedding.
Written in Go
You can use contented as a standalone upload server, or you can use the SDK to embed its upload widget into another website.
=FEATURES=
- Drag and drop uploader, or fallback classic uploader, or pastebin-style uploader
- Multiple files upload
- Pastebin upload
- SDK-oriented design for embedding, including CORS support
- Mobile friendly HTML interface
- Preserves uploaded filename and content-type metadata
- Hash verification (SHA512/256)
- Detect duplicate upload content and reuse storage
- Options to limit the upload filesize and the upload bandwidth
- Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm)
=USAGE (SERVER)=
`Usage of contented:
-data string
Directory for stored content (default "")
-db string
Path for metadata database (default "contented.db")
-listen string
(default "127.0.0.1:80")
-max int
Maximum size of uploaded files in MiB (set zero for unlimited) (default 8)
-speed int
Maximum upload speed in bytes/sec (set zero for unlimited)
-title string
Title used in web interface (default "contented")
`
=USAGE (HTTP)=
The server responds on the following URLs:
- `/get/{ID}`: Download item content
- `/info/{ID}`: Get item content metadata (JSON)
- `/about`: Get server metadata (JSON)
=USAGE (EMBEDDING FOR WEB)=
Your webpage should load the SDK from the contented server, then call the `contented.init` function to display the upload widget over the top of an existing DOM element.
The SDK will run your callback, passing it the file IDs of any uploaded items.
The SDK depends on jQuery.
`
<script type="text/javascript" src="SERVER_ADDR/sdk.js"></script>
contented.init("#target", function(/* String[] */ items) {});
`
=CHANGELOG=
2017-10-08: 1.0.0
- Initial public release

View File

@@ -18,13 +18,21 @@ func main() {
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")
flag.Parse()
svr, err := contented.NewServer(&contented.ServerOptions{
DataDirectory: *dataDir,
DBPath: *dbPath,
BandwidthLimit: int64(*maxUploadSpeed),
DataDirectory: *dataDir,
DBPath: *dbPath,
BandwidthLimit: int64(*maxUploadSpeed),
TrustXForwardedFor: *trustXForwardedFor,
EnableHomepage: *enableHomepage,
DiskFilesWorldReadable: *diskFilesWorldReadable,
MaxConcurrentThumbs: *maxConcurrentThumbs,
ServerPublicProperties: contented.ServerPublicProperties{
AppTitle: *appTitle,
MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024,

BIN
doc/image1.png Normal file

Binary file not shown.

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

@@ -10,7 +10,7 @@ import (
func (this *Server) handleView(w http.ResponseWriter, r *http.Request, fileID string) {
err := this.handleViewInternal(w, r, r.URL.Path[len(downloadUrlPrefix):])
if err != nil {
log.Printf("%s View failed: %s\n", r.RemoteAddr, err.Error())
log.Printf("%s View failed: %s\n", this.remoteIP(r), err.Error())
if os.IsNotExist(err) {
http.Error(w, "File not found", 404)
} else {
@@ -37,9 +37,17 @@ func (this *Server) handleViewInternal(w http.ResponseWriter, r *http.Request, f
// ServeContent only uses the filename to get the mime type, which we can
// 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+`"`)
default:
w.Header().Set(`Content-Type`, m.MimeType)
}
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))
}

5
static/drawingboard-0.4.6.min.css vendored Normal file

File diff suppressed because one or more lines are too long

4
static/drawingboard-0.4.6.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -7,9 +7,23 @@
<style type="text/css">
html, body {
font-family: sans-serif;
margin:0;
width:100%;
height:100%;
}
#padder {
height:100%;
position:relative;
}
#surrogate-area {
height:300px;
position:absolute;
top:10px;
bottom:10px;
left:10px;
right:10px;
}
button.again {
margin-top:2em;
}
/* hide close button */
.contented-close {
@@ -18,9 +32,11 @@ html, body {
</style>
</head>
<body>
<div id="surrogate-area">
Loading...
</div>
<div id="padder">
<div id="surrogate-area">
Loading...
</div>
</div>
<script type="text/javascript" src="/jquery-1.12.4.min.js"></script>
<script type="text/javascript" src="/sdk.js"></script>
@@ -40,17 +56,7 @@ $.get("/about", function(ret) {
// Load upload widget
contented.init("#surrogate-area", function(items) {
var $table = $("<table>");
for (var i = 0; i < items.length; ++i) {
$table.append($("<tr>").append([
$("<td>").text(items[i]),
$("<td>").html("<a target='_blank' href='/get/" + items[i] + "'>get</a>"),
$("<td>").html("<a target='_blank' href='/info/" + items[i] + "'>info</a>")
]))
}
$("#surrogate-area").html($table);
window.location.href = contented.getMultiPreviewURL(items);
});
</script>
</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

@@ -1,214 +1,29 @@
;
var contented = (function ($, currentScriptPath) {
"use strict";
var baseURL = currentScriptPath.replace('sdk.js', '');
//
var formatBytes = function(bytes) {
if (bytes < 1024) {
return bytes + " B";
} else if (bytes < (1024*1024)) {
return (bytes / 1024).toFixed(1) + " KiB";
} else if (bytes < (1024*1024*1024)) {
return (bytes / (1024*1024)).toFixed(1) + " MiB";
} else {
return (bytes / (1024*1024*1024)).toFixed(1) + " GiB";
}
};
var contented = (function() {
"use strict";
/**
* supportsDrop returns whether drag-and-drop is supported by this browser.
*
* @return bool
*/
var supportsDrop = function () {
return ('ondrop' in window && 'FormData' in window && 'FileReader' in window);
}
// @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) {
/**
* 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
*/
var initArea = function (elementSelector, onUploaded, onClose) {
onUploaded = onUploaded || function () { };
onClose = onClose || function () { };
var binStr = atob( this.toDataURL(type, quality).split(',')[1] ),
len = binStr.length,
arr = new Uint8Array(len);
if ($(elementSelector).length != 1) {
return; // should only find one element
}
var element = $(elementSelector)[0];
for (var i = 0; i < len; i++ ) {
arr[i] = binStr.charCodeAt(i);
}
// <input type="hidden" name="MAX_FILE_SIZE" value="` + ret.MaxUploadBytes + `" />
// Create a new div for ourselves on top of the existing area
$.get(baseURL + "about", function (ret) {
var extraText = "";
if (ret.MaxUploadBytes > 0) {
extraText = " (max " + formatBytes(ret.MaxUploadBytes) + ")";
}
$.get(baseURL + "widget.html", function (widgetHtml) {
var $f = $("<div>").html(widgetHtml);
$f.find(".contented-extratext").text(extraText);
var ourClose = function () {
$f.remove(); // remove from dom
onClose(); // upstream close
};
$f.find(".contented-close").click(function () {
ourClose();
})
var setType = function (type) {
$f.find(".contented-upload-type").removeClass("contented-upload-type-active");
$f.find(".contented-upload-type[data-upload-type=" + type + "]").addClass("contented-upload-type-active");
$f.find(".contented-upload-if").removeClass("contented-active");
$f.find(".contented-if-" + type).addClass("contented-active");
};
$f.find(".contented-upload-type").click(function () {
setType($(this).attr('data-upload-type'));
});
if (!supportsDrop()) {
// switch default
setType('file');
}
//
var $element = $(element);
var offset = $element.offset();
$f.css({
'position': 'absolute',
'left': offset.left + "px",
'top': offset.top + "px",
'width': $element.width() + "px",
'min-width': $element.width() + "px",
'max-width': $element.width() + "px",
'height': $element.height() + "px",
'min-height': $element.height() + "px",
'max-height': $element.height() + "px"
});
$f.find('.contented').on('dragover dragenter', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).addClass('is-dragging');
});
$f.find('.contented').on('dragleave dragend', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('is-dragging');
});
$f.find('.contented').on('drop', function (e) {
e.preventDefault();
e.stopPropagation();
handleUploadFrom(e.originalEvent.dataTransfer.files);
});
$f.find('.contented-file-upload').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
handleUploadFrom($(".contented-file-selector")[0].files);
});
$f.find('.contented-paste-upload').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var blob = new Blob([$(".contented-if-paste textarea").val()], {type : 'text/plain'});
handleUploadFrom([blob]);
});
$("body").append($f);
var setProgressCaption = function(message) {
$f.find(".contented-if-progress label").text(message);
};
var setProgressPercentage = function(frc) {
$f.find(".contented-progress-element").css('width', (frc * 100) + "%");
};
var handleUploadFrom = function (files) {
setProgressCaption("Uploading, please wait...");
setProgressPercentage(0);
setType("progress");
$f.find(".contented-upload-type-selector").hide();
$f.find(".contented").removeClass('is-dragging');
// Ajax uploader
var ajaxData = new FormData();
for (var i = 0; i < files.length; ++i) {
ajaxData.append("f", files[i]);
}
// ajax request
$.ajax({
url: baseURL + "upload",
type: "POST",
data: ajaxData,
dataType: 'json', // response type
cache: false,
contentType: false,
processData: false,
xhr: function() {
var xhr = $.ajaxSettings.xhr();
xhr.upload.addEventListener(
'progress',
function(ev) {
console.log([ev.lengthComputable, ev.loaded, ev.total]);
if (ev.lengthComputable) {
setProgressCaption("Uploading (" + formatBytes(ev.loaded) + " / " + formatBytes(ev.total) + ")...");
setProgressPercentage(ev.total == 0 ? 0 : ev.loaded / ev.total);
}
},
false
);
return xhr;
},
complete: function () {
setProgressCaption("Upload complete.");
setProgressPercentage(1);
},
success: function (data) {
onUploaded(data);
ourClose();
},
error: function () {
setProgressCaption("Upload failed.");
}
});
}
});
});
}
//
return {
'supportsDrop': supportsDrop,
'init': initArea
};
})(
jQuery,
(function () {
"use strict";
callback( new Blob( [arr], {type: type || 'image/png'} ) );
}
});
}
var getCurrentScriptPath = function () {
// Determine current script path
// @ref https://stackoverflow.com/a/26023176
var scripts = document.querySelectorAll('script[src]');
@@ -217,5 +32,433 @@ var contented = (function ($, currentScriptPath) {
var currentScriptFile = currentScriptChunks[currentScriptChunks.length - 1];
return currentScript.replace(currentScriptFile, '');
})()
);
};
var currentScriptPath = getCurrentScriptPath();
var baseURL = currentScriptPath.replace('sdk.js', '');
return {
"loaded": false,
"baseURL": baseURL,
"__preInit": [],
/**
* initArea shows the contented upload widget over the top of a target DOM element.
*
* @param any element Drop target (string selector / DOMElement / jQuery)
* @param Function onUploaded Called with an array of upload IDs
* @param Function onClose Called when the widget is being destroyed
*/
"init": function(elementSelector, onUploaded, onClose) {
contented.__preInit.push([elementSelector, onUploaded, onClose]);
},
/**
* supportsDrop returns whether drag-and-drop is supported by this browser.
*
* @return bool
*/
"supportsDrop": function() {
return ('ondrop' in window && 'FormData' in window && 'FileReader' in window);
},
"getPreviewURL": function(id) {
return baseURL + "p/" + encodeURIComponent(id);
},
"getMultiPreviewURL": function(items) {
return baseURL + "p/" + encodeURIComponent(items.join("-"));
},
"getDownloadURL": function(id) {
return baseURL + "get/" + encodeURIComponent(id);
},
"getInfoJSONURL": function(id) {
return baseURL + "info/" + encodeURIComponent(id);
},
"getThumbnailURL": function(thumbnailType, id) {
return baseURL + "thumb/" + encodeURIComponent(thumbnailType) + "/" + encodeURIComponent(id);
},
"thumbnail": {
"small_square": "s",
"medium_square": "b",
"medium": "t",
"large": "m",
"xlarge": "l",
"xxlarge": "h"
}
};
})();
;(function() {
"use strict";
var loadScript = function(url, onLoad) {
var script = document.createElement('script');
script.onload = onLoad;
script.src = url;
document.head.appendChild(script);
};
var loadScripts = function(urls, onLoad) {
// load sequentially
var i = 0;
var loadNext = function() {
if (i === urls.length) {
onLoad();
return;
}
var url = urls[i];
i += 1;
loadScript(url, loadNext);
};
loadNext();
};
var formatBytes = function(bytes) {
var k = 1024, m = (1024*1024), g = (1024*1024*1024);
if (bytes < k) {
return bytes + " B";
} else if (bytes < m) {
return (bytes / k).toFixed(1) + " KiB";
} else if (bytes < g) {
return (bytes / m).toFixed(1) + " MiB";
} else {
return (bytes / g).toFixed(1) + " GiB";
}
};
// @ref https://stackoverflow.com/a/2117523
var guid = function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
var afterScriptsLoaded = function() {
var initArea = function (elementSelector, onUploaded, onClose) {
onUploaded = onUploaded || function () { };
onClose = onClose || function () { };
if ($(elementSelector).length != 1) {
return; // should only find one element
}
var element = $(elementSelector)[0];
// <input type="hidden" name="MAX_FILE_SIZE" value="` + ret.MaxUploadBytes + `" />
// Create a new div for ourselves on top of the existing area
$.get(contented.baseURL + "about", function (ret) {
var extraText = "";
if (ret.MaxUploadBytes > 0) {
extraText = " (max " + formatBytes(ret.MaxUploadBytes) + ")";
}
$.get(contented.baseURL + "widget.html", function (widgetHtml) {
var $f = $("<div>").html(widgetHtml);
$f.find(".contented-extratext").text(extraText);
// Tab buttons
var hasSetupDrawingBoardYet = false;
var setType = function (type) {
$f.find(".contented-upload-type").removeClass("contented-upload-type-active");
$f.find(".contented-upload-type[data-upload-type=" + type + "]").addClass("contented-upload-type-active");
$f.find(".contented-upload-if").removeClass("contented-active");
$f.find(".contented-if-" + type).addClass("contented-active");
if (type == "drag") {
enablePasteHandler();
} else {
disablePasteHandler();
}
if (type == "drawing" && !hasSetupDrawingBoardYet) {
setupDrawingBoard();
hasSetupDrawingBoardYet = true;
}
};
$f.find(".contented-upload-type").click(function () {
setType($(this).attr('data-upload-type'));
});
// Widget positioning
var $element = $(element);
var offset = $element.offset();
$f.css({
'position': 'absolute',
'left': offset.left + "px",
'top': offset.top + "px",
'width': $element.width() + "px",
'min-width': $element.width() + "px",
'max-width': $element.width() + "px",
'height': $element.height() + "px",
'min-height': $element.height() + "px",
'max-height': $element.height() + "px"
});
// Drag and drop support
$f.find('.contented').on('dragover dragenter', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).addClass('is-dragging');
});
$f.find('.contented').on('dragleave dragend', function (e) {
e.preventDefault();
e.stopPropagation();
$(this).removeClass('is-dragging');
});
$f.find('.contented').on('drop', function (e) {
e.preventDefault();
e.stopPropagation();
handleUploadFrom(e.originalEvent.dataTransfer.files);
});
$f.find('.contented-file-upload').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
handleUploadFrom($(".contented-file-selector")[0].files);
});
// Pastebin
$f.find('.contented-paste-upload').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
var blob = new Blob([$(".contented-if-paste textarea").val()], {type : 'text/plain'});
handleUploadFrom([blob]);
});
// Ctrl+V uploads
var pasteHandler = function(e) {
e.preventDefault();
e.stopPropagation();
var items = (e.clipboardData || e.originalEvent.clipboardData).items;
var items_length = items.length;
var blobs = [];
var handled = 0;
var haveHandled = function() {
handled += 1;
if (handled == items_length) {
if (blobs.length > 0) {
handleUploadFrom( blobs );
} else {
// alert("Pasted 0 files");
}
}
};
for (var i = 0; i < items.length; ++i) {
var item = items[i];
var mimeType = item.type;
if (item.kind === 'file') {
blobs.push(item.getAsFile());
haveHandled();
} else if (item.kind === 'string') {
item.getAsString(function(s) {
blobs.push( new Blob([s], {type : mimeType}) );
haveHandled();
});
} else {
// file|string are the only supported types in
// all browsers at the time of writing
// Ignore future possibilities
haveHandled();
}
}
};
var enablePasteHandler = function() {
document.addEventListener('paste', pasteHandler);
};
var disablePasteHandler = function() {
document.removeEventListener('paste', pasteHandler);
};
// Embed in DOM, load default area
$("body").append($f);
if (!contented.supportsDrop()) {
setType('file');
} else {
setType('drag');
}
// Drawing board
var setupDrawingBoard = function() {
$("head").append(
'<link rel="stylesheet" type="text/css" href="' + contented.baseURL + 'drawingboard-0.4.6.min.css">'
);
var db_id = "contented-drawing-area-" + guid();
var $db = $("<div>").attr('id', db_id);
DrawingBoard.Control.ContentedUpload = DrawingBoard.Control.extend({
name: 'upload',
initialize: function() {
var $el = this.$el;
$el.append('<button class="contented-drawingboard-upload">Upload</button>');
$el.on('click', '.contented-drawingboard-upload', $.proxy(function(e) {
e.preventDefault();
e.stopPropagation();
$el.prop('disabled', true);
$el.text('Saving...');
$db.find("canvas")[0].toBlob(function(theBlob) {
handleUploadFrom([ theBlob ]);
});
}, this));
}
});
$db.css({
//'width': $f.find(".contented-content-area").width(),
'height': $f.find(".contented-content-area").height(),
'overflow': 'hidden'
});
$f.find(".contented-drawing-area").append($db);
var db = new DrawingBoard.Board(db_id, {
'controls': [
'Color',
'Size',
'DrawingMode',
'Navigation',
'ContentedUpload'
],
'controlsPosition': 'center',
'enlargeYourContainer': false,
'webStorage': false,
'droppable': false // don't mess with existing drop support
});
};
// Close button
var ourClose = function () {
$f.remove(); // remove from dom
disablePasteHandler();
onClose(); // upstream close
};
$f.find(".contented-close").click(function () {
ourClose();
})
// Progress bar
var setProgressCaption = function(message) {
$f.find(".contented-if-progress label").text(message);
};
var setProgressPercentage = function(frc) {
$f.find(".contented-progress-element").css('width', (frc * 100) + "%");
};
// Common upload handler
var handleUploadFrom = function(files) {
setProgressCaption("Uploading, please wait...");
setProgressPercentage(0);
setType("progress");
$f.find(".contented-upload-type-selector").hide();
$f.find(".contented").removeClass('is-dragging');
// Ajax uploader
var ajaxData = new FormData();
for (var i = 0; i < files.length; ++i) {
ajaxData.append("f", files[i]);
}
// ajax request
$.ajax({
url: contented.baseURL + "upload",
type: "POST",
data: ajaxData,
dataType: 'json', // response type
cache: false,
contentType: false,
processData: false,
xhr: function() {
var xhr = $.ajaxSettings.xhr();
xhr.upload.addEventListener(
'progress',
function(ev) {
if (ev.lengthComputable) {
setProgressCaption("Uploading (" + formatBytes(ev.loaded) + " / " + formatBytes(ev.total) + ")...");
setProgressPercentage(ev.total == 0 ? 0 : ev.loaded / ev.total);
}
},
false
);
return xhr;
},
complete: function () {
setProgressPercentage(1);
},
success: function (data) {
setProgressCaption("Upload completed successfully.");
onUploaded(data);
ourClose();
},
error: function () {
setProgressCaption("Upload failed!");
}
});
}
// .
});
});
}
// Update fields in global variable
contented.init = initArea;
contented.loaded = true;
// Call initArea for all pre-initialised elements
for (var i = 0; i < contented.__preInit.length; ++i) {
initArea(contented.__preInit[i][0], contented.__preInit[i][1], contented.__preInit[i][2]);
}
};
// Load scripts
var needScripts = [];
if (typeof jQuery === "undefined") {
needScripts.push(contented.baseURL + "jquery-1.12.4.min.js");
}
if (typeof DrawingBoard === "undefined") {
needScripts.push(contented.baseURL + "drawingboard-0.4.6.min.js");
}
loadScripts(needScripts, afterScriptsLoaded);
})()

View File

@@ -24,6 +24,7 @@
.contented .contented-upload-type-selector {
display:block;
margin-bottom: 1em;
-webkit-user-select: none;
user-select: none;
}
.contented .contented-upload-type {
@@ -66,7 +67,7 @@
.contented-upload-if {
display:none;
}
.contented-if-paste {
.contented-if-paste, .contented-if-drawing {
height:100%;
}
.contented-upload-if.contented-active {
@@ -87,12 +88,14 @@
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">
@@ -122,11 +125,17 @@
<path fill="#000000" d="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z" />
</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 to upload <span class="contented-extratext"></span></label>
<label>Drop files or Ctrl-V to upload <span class="contented-extratext"></span></label>
</div>
<div class="contented-upload-if contented-if-file">
@@ -140,6 +149,10 @@
<button class="contented-paste-upload">Upload &raquo;</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>

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

@@ -18,15 +18,17 @@ import (
func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
err := r.ParseMultipartForm(this.opts.MaxUploadBytes * 2)
remoteIP := this.remoteIP(r)
err := r.ParseMultipartForm(0) // buffer upload in temporary files on disk, not memory
if err != nil {
log.Printf("%s Invalid request: %s\n", r.RemoteAddr, err.Error())
log.Printf("%s Invalid request: %s\n", remoteIP, err.Error())
http.Error(w, "Invalid request", 400)
return
}
if r.MultipartForm == nil || r.MultipartForm.File == nil || len(r.MultipartForm.File["f"]) < 1 {
log.Printf("%s Invalid request: no multipart content\n", r.RemoteAddr)
log.Printf("%s Invalid request: no multipart content\n", remoteIP)
http.Error(w, "Invalid request", 400)
return
}
@@ -36,14 +38,14 @@ func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
for _, fhs := range r.MultipartForm.File["f"] {
f, err := fhs.Open()
if err != nil {
log.Printf("%s Internal error: %s\n", r.RemoteAddr, err.Error())
log.Printf("%s Internal error: %s\n", remoteIP, err.Error())
http.Error(w, "Internal error", 500)
return
}
path, err := this.handleUploadFile(f, fhs, remoteIP(r))
path, err := this.handleUploadFile(f, fhs, remoteIP)
if err != nil {
log.Printf("%s Upload failed: %s\n", r.RemoteAddr, err.Error())
log.Printf("%s Upload failed: %s\n", remoteIP, err.Error())
http.Error(w, "Upload failed", 500)
}
@@ -52,7 +54,7 @@ func (this *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
jb, err := json.Marshal(ret)
if err != nil {
log.Printf("%s Internal error: %s\n", r.RemoteAddr, err.Error())
log.Printf("%s Internal error: %s\n", remoteIP, err.Error())
http.Error(w, "Internal error", 500)
return
}
@@ -89,7 +91,7 @@ func (this *Server) handleUploadFile(src multipart.File, hdr *multipart.FileHead
// 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, 0600)
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