21 Commits

Author SHA1 Message Date
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
17 changed files with 369 additions and 44 deletions

View File

@@ -7,3 +7,5 @@ c7b699105bd166e7a01aa0e678d34624680bf81e release-1.1.0
cfb1e028fd0627614aa01184893f9f29f20a347e release-1.1.0 cfb1e028fd0627614aa01184893f9f29f20a347e release-1.1.0
cfb1e028fd0627614aa01184893f9f29f20a347e release-1.1.0 cfb1e028fd0627614aa01184893f9f29f20a347e release-1.1.0
0000000000000000000000000000000000000000 release-1.1.0 0000000000000000000000000000000000000000 release-1.1.0
0000000000000000000000000000000000000000 release-1.1.0
98da2ebf0d50dffe8b625457a639bc2f15519714 release-1.1.0

View File

@@ -2,7 +2,7 @@
# Makefile for contented # Makefile for contented
# #
VERSION:=1.1.0 VERSION:=1.2.0
SOURCES:=Makefile \ SOURCES:=Makefile \
static \ static \

View File

@@ -5,6 +5,8 @@ import (
"encoding/json" "encoding/json"
"log" "log"
"net/http" "net/http"
"os"
"regexp"
"strings" "strings"
"time" "time"
@@ -22,12 +24,21 @@ type ServerPublicProperties struct {
type ServerOptions struct { type ServerOptions struct {
DataDirectory string DataDirectory string
DBPath string DBPath string
DiskFilesWorldReadable bool
BandwidthLimit int64 BandwidthLimit int64
TrustXForwardedFor bool TrustXForwardedFor bool
EnableHomepage bool EnableHomepage bool
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
@@ -90,8 +101,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 +124,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

@@ -6,8 +6,9 @@ TODO
- Encrypted at rest (anti- provider snooping) - 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) - Prevent selecting around the toggle buttons (firefox for android)
- Detect when pasting creates one large file and one ~150 byte file(?? why does this happen?)
- Detect when pasting an image URL, offer to download it (clientside?)

View File

@@ -18,6 +18,7 @@ You can use contented as a standalone upload server, or you can use the SDK to e
- Detect duplicate upload content and reuse storage - Detect duplicate upload content and reuse storage
- Options to limit the upload filesize and the upload bandwidth - Options to limit the upload filesize and the upload bandwidth
- Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm) - Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm)
- Image thumbnailing
=USAGE (SERVER)= =USAGE (SERVER)=
@@ -26,10 +27,12 @@ You can use contented as a standalone upload server, or you can use the SDK to e
Directory for stored content (default "") Directory for stored content (default "")
-db string -db string
Path for metadata database (default "contented.db") Path for metadata database (default "contented.db")
-diskFilesWorldReadable
Save files as 0644 instead of 0600
-enableHomepage -enableHomepage
Enable homepage (disable for embedded use only) (default true) Enable homepage (disable for embedded use only) (default true)
-listen string -listen string
(default "127.0.0.1:80") IP/Port to bind server (default "127.0.0.1:80")
-max int -max int
Maximum size of uploaded files in MiB (set zero for unlimited) (default 8) Maximum size of uploaded files in MiB (set zero for unlimited) (default 8)
-speed int -speed int
@@ -48,6 +51,7 @@ The server responds on the following URLs:
- `/get/{ID}`: Download item content - `/get/{ID}`: Download item content
- `/info/{ID}`: Get item content metadata (JSON) - `/info/{ID}`: Get item content metadata (JSON)
- `/thumb/{Type}/{ID}`: Get item thumbnail image
- `/about`: Get server metadata (JSON) - `/about`: Get server metadata (JSON)
=USAGE (EMBEDDING FOR WEB)= =USAGE (EMBEDDING FOR WEB)=
@@ -60,6 +64,12 @@ contented.init("#target", function(/* String[] */ items) {});
=CHANGELOG= =CHANGELOG=
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
2017-10-15: 1.1.0 2017-10-15: 1.1.0
- Feature: Drawing mode - Feature: Drawing mode
- Feature: Ctrl+V image upload - Feature: Ctrl+V image upload

View File

@@ -20,6 +20,7 @@ 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")
flag.Parse() flag.Parse()
@@ -29,6 +30,7 @@ func main() {
BandwidthLimit: int64(*maxUploadSpeed), BandwidthLimit: int64(*maxUploadSpeed),
TrustXForwardedFor: *trustXForwardedFor, TrustXForwardedFor: *trustXForwardedFor,
EnableHomepage: *enableHomepage, EnableHomepage: *enableHomepage,
DiskFilesWorldReadable: *diskFilesWorldReadable,
ServerPublicProperties: contented.ServerPublicProperties{ ServerPublicProperties: contented.ServerPublicProperties{
AppTitle: *appTitle, AppTitle: *appTitle,
MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024, MaxUploadBytes: int64(*maxUploadMb) * 1024 * 1024,

97
preview.go Normal file
View File

@@ -0,0 +1,97 @@
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>
<head>
<title>` + html.EscapeString(this.opts.ServerPublicProperties.AppTitle) + `</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<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"
} }
}; };

File diff suppressed because one or more lines are too long

78
thumb.go Normal file
View File

@@ -0,0 +1,78 @@
package contented
import (
"errors"
"fmt"
"log"
"net/http"
"path/filepath"
"code.ivysaur.me/thumbnail"
)
func thumbnailer(t byte) (*thumbnail.Thumbnailer, error) {
// Modelled on what imgur.com offers
// @ref https://api.imgur.com/models/image#thumbs
const (
cacheSize = 1
outputFmt = thumbnail.OUTPUT_JPG
scaleFmt = thumbnail.SCALEFMT_BILINEAR
)
switch t {
case 's':
return thumbnail.NewThumbnailerEx(90, 90, cacheSize, outputFmt, thumbnail.ASPECT_CROP_TO_DIMENSIONS, scaleFmt), nil
case 'b':
return thumbnail.NewThumbnailerEx(160, 160, cacheSize, outputFmt, thumbnail.ASPECT_CROP_TO_DIMENSIONS, scaleFmt), nil
case 't':
return thumbnail.NewThumbnailerEx(160, 160, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
case 'm':
return thumbnail.NewThumbnailerEx(340, 340, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
case 'l':
return thumbnail.NewThumbnailerEx(640, 640, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
case 'h':
return thumbnail.NewThumbnailerEx(1024, 1024, cacheSize, outputFmt, thumbnail.ASPECT_RESPECT_MAX_DIMENSION_ONLY, scaleFmt), nil
default:
return nil, errors.New("Unsupported thumbnail type (should be s/b/t/m/l/h)")
}
}
func (this *Server) handleThumb(w http.ResponseWriter, r *http.Request, thumbnailType byte, fileId string) {
t, err := thumbnailer(thumbnailType)
if err != nil {
log.Printf("%s Thumbnail failed: %s\n", this.remoteIP(r), err.Error())
http.Error(w, err.Error(), 400)
return
}
err = this.handleThumbInternal(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`, t.Height()))
w.WriteHeader(302)
}
}
func (this *Server) handleThumbInternal(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.RenderFile_NoCache_MimeType(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

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