Add support for file uploading #16
							
								
								
									
										3
									
								
								assets/templates/html/deletePasteSuccess.html.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								assets/templates/html/deletePasteSuccess.html.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | {{define "title"}} | ||||||
|  | Success - rushlink | ||||||
|  | {{end}} | ||||||
| @ -8,6 +8,9 @@ the command line. | |||||||
| 
 | 
 | ||||||
| ## USAGE | ## USAGE | ||||||
| 
 | 
 | ||||||
|  |     # Upload a file | ||||||
|  |     curl -F'file=@yourfile.png' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> | ||||||
|  | 
 | ||||||
|     # Shorten a URL |     # Shorten a URL | ||||||
|     curl -F'shorten=http://example.com/some/long/url' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> |     curl -F'shorten=http://example.com/some/long/url' <a href="//{{.Request.Host}}">https://{{.Request.Host}}</a> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,3 @@ | |||||||
|  | {{define "title"}} | ||||||
|  | Success - rushlink | ||||||
|  | {{end}} | ||||||
							
								
								
									
										1
									
								
								assets/templates/txt/deletePasteSuccess.txt.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								assets/templates/txt/deletePasteSuccess.txt.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | |||||||
|  | <{{.Request.Host}}/{{.Paste.Key}}> was succesfully deleted | ||||||
| @ -6,6 +6,9 @@ the command line. | |||||||
| 
 | 
 | ||||||
| ## USAGE | ## USAGE | ||||||
| 
 | 
 | ||||||
|  |     # Upload a file | ||||||
|  |     curl -F'file=@yourfile.png' https://{{.Request.Host}} | ||||||
|  | 
 | ||||||
|     # Shorten a URL |     # Shorten a URL | ||||||
|     curl -F'shorten=http://example.com/some/long/url' https://{{.Request.Host}} |     curl -F'shorten=http://example.com/some/long/url' https://{{.Request.Host}} | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										5
									
								
								assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								assets/templates/txt/newFileUploadPasteSuccess.txt.tmpl
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | {{if .Request.PostForm.deleteToken -}} | ||||||
|  | https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} | ||||||
|  | {{else -}} | ||||||
|  | https://{{.Request.Host}}/{{.Paste.Key}} | ||||||
|  | {{end -}} | ||||||
| @ -1,4 +1,5 @@ | |||||||
| https://{{.Request.Host}}/{{.Paste.Key}} |  | ||||||
| {{if .Request.PostForm.deleteToken -}} | {{if .Request.PostForm.deleteToken -}} | ||||||
| https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} | https://{{.Request.Host}}/{{.Paste.Key}}?deleteToken={{.Paste.DeleteToken | urlquery}} | ||||||
|  | {{else -}} | ||||||
|  | https://{{.Request.Host}}/{{.Paste.Key}} | ||||||
| {{end -}} | {{end -}} | ||||||
|  | |||||||
| @ -9,6 +9,7 @@ import ( | |||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	databasePath  = flag.String("database", "", "location of the database file") | 	databasePath  = flag.String("database", "", "location of the database file") | ||||||
|  | 	fileStorePath = flag.String("file-store", "", "path to the directory where uploaded files will be stored") | ||||||
| 	httpListen    = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)") | 	httpListen    = flag.String("listen", "127.0.0.1:8000", "listen address (host:port)") | ||||||
| 	metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)") | 	metricsListen = flag.String("metrics_listen", "127.0.0.1:58614", "listen address for metrics (host:port)") | ||||||
| ) | ) | ||||||
| @ -16,10 +17,13 @@ var ( | |||||||
| func main() { | func main() { | ||||||
| 	flag.Parse() | 	flag.Parse() | ||||||
| 
 | 
 | ||||||
| 	if err := rushlink.Open(*databasePath); err != nil { | 	if err := rushlink.OpenDB(*databasePath); err != nil { | ||||||
|  | 		log.Fatalln(err) | ||||||
|  | 	} | ||||||
|  | 	defer rushlink.CloseDB() | ||||||
|  | 	if err := rushlink.OpenFileStore(*fileStorePath); err != nil { | ||||||
| 		log.Fatalln(err) | 		log.Fatalln(err) | ||||||
| 	} | 	} | ||||||
| 	defer rushlink.Close() |  | ||||||
| 
 | 
 | ||||||
| 	go rushlink.StartMetricsServer(*metricsListen) | 	go rushlink.StartMetricsServer(*metricsListen) | ||||||
| 	rushlink.StartMainServer(*httpListen) | 	rushlink.StartMainServer(*httpListen) | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								db.go
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								db.go
									
									
									
									
									
								
							| @ -15,7 +15,7 @@ var DB *bolt.DB | |||||||
| // | // | ||||||
| // If we alter the database format, we bump this number and write a new | // If we alter the database format, we bump this number and write a new | ||||||
| // database migration in migrateDatabase(). | // database migration in migrateDatabase(). | ||||||
| const CURRENT_MIGRATE_VERSION = 1 | const CURRENT_MIGRATE_VERSION = 2 | ||||||
| 
 | 
 | ||||||
| // Bucket storing everything that is not a bulk value. This includes stuff like | // Bucket storing everything that is not a bulk value. This includes stuff like | ||||||
| // the database version, secret site-wide keys. | // the database version, secret site-wide keys. | ||||||
| @ -24,12 +24,15 @@ const BUCKET_CONF = "conf" | |||||||
| // The main bucket for paste values and URL redirects | // The main bucket for paste values and URL redirects | ||||||
| const BUCKET_PASTES = "pastes" | const BUCKET_PASTES = "pastes" | ||||||
| 
 | 
 | ||||||
|  | // The main bucket for file uploads | ||||||
|  | const BUCKET_FILE_UPLOAD = "fileUpload" | ||||||
|  | 
 | ||||||
| // This value stores the current migration version. If this value is less than | // This value stores the current migration version. If this value is less than | ||||||
| // CURRENT_MIGRATE_VERSION, the database has to be migrated. | // CURRENT_MIGRATE_VERSION, the database has to be migrated. | ||||||
| const KEY_MIGRATE_VERSION = "migrate_version" | const KEY_MIGRATE_VERSION = "migrate_version" | ||||||
| 
 | 
 | ||||||
| // Open the bolt database | // Open the bolt database | ||||||
| func Open(path string) error { | func OpenDB(path string) error { | ||||||
| 	if path == "" { | 	if path == "" { | ||||||
| 		return errors.New("database not set") | 		return errors.New("database not set") | ||||||
| 	} | 	} | ||||||
| @ -43,7 +46,7 @@ func Open(path string) error { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Close the bolt database | // Close the bolt database | ||||||
| func Close() error { | func CloseDB() error { | ||||||
| 	if DB == nil { | 	if DB == nil { | ||||||
| 		panic("no open database") | 		panic("no open database") | ||||||
| 	} | 	} | ||||||
| @ -60,25 +63,35 @@ func migrateDatabase(tx *bolt.Tx) error { | |||||||
| 	// Migrate the database to version 1 | 	// Migrate the database to version 1 | ||||||
| 	if dbVersion < 1 { | 	if dbVersion < 1 { | ||||||
| 		log.Println("migrating database to version 1") | 		log.Println("migrating database to version 1") | ||||||
| 
 |  | ||||||
| 		// Create conf bucket | 		// Create conf bucket | ||||||
| 		_, err := tx.CreateBucket([]byte(BUCKET_CONF)) | 		_, err := tx.CreateBucket([]byte(BUCKET_CONF)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		// Create paste bucket | 		// Create paste bucket | ||||||
| 		_, err = tx.CreateBucket([]byte(BUCKET_PASTES)) | 		_, err = tx.CreateBucket([]byte(BUCKET_PASTES)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		// Update the version number | 		// Update the version number | ||||||
| 		if err := setDBVersion(tx, 1); err != nil { | 		if err := setDBVersion(tx, 1); err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	if dbVersion < 2 { | ||||||
|  | 		log.Println("migrating database to version 2") | ||||||
|  | 		// Create fileUpload bucket | ||||||
|  | 		_, err := tx.CreateBucket([]byte(BUCKET_FILE_UPLOAD)) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		// Update the version number | ||||||
|  | 		if err := setDBVersion(tx, 2); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										194
									
								
								fileupload.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										194
									
								
								fileupload.go
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,194 @@ | |||||||
|  | package rushlink | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"encoding/hex" | ||||||
|  | 	"hash/crc32" | ||||||
|  | 	"io" | ||||||
|  | 	"log" | ||||||
|  | 	"net/http" | ||||||
|  | 	"net/url" | ||||||
|  | 	"os" | ||||||
|  | 	"path" | ||||||
|  | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
|  | 	"github.com/pkg/errors" | ||||||
|  | 	bolt "go.etcd.io/bbolt" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // Use the Castagnoli checksum because of the acceleration on Intel CPUs | ||||||
|  | var checksumTable = crc32.MakeTable(crc32.Castagnoli) | ||||||
|  | 
 | ||||||
|  | // Where to store the uploaded files | ||||||
|  | var fileStoreDir = "" | ||||||
|  | 
 | ||||||
|  | // Custom HTTP filesystem handler | ||||||
|  | type fileUploadFileSystem struct { | ||||||
|  | 	fs http.FileSystem | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Open opens file | ||||||
|  | func (fs fileUploadFileSystem) Open(path string) (http.File, error) { | ||||||
|  | 	log.Println(path) | ||||||
|  | 	file, err := fs.fs.Open(path) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.Wrap(err, "opening file") | ||||||
|  | 	} | ||||||
|  | 	stat, err := file.Stat() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.Wrap(err, "file.Stat()") | ||||||
|  | 	} | ||||||
|  | 	if stat.IsDir() { | ||||||
|  | 		return nil, errors.New("directory index not allowed") | ||||||
|  | 	} | ||||||
|  | 	return file, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type fileUploadState int | ||||||
|  | 
 | ||||||
|  | type fileUpload struct { | ||||||
|  | 	State       fileUploadState | ||||||
|  | 	ID          uuid.UUID | ||||||
|  | 	FileName    string | ||||||
|  | 	ContentType string | ||||||
|  | 	Checksum    uint32 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	dirMode  os.FileMode = 0750 | ||||||
|  | 	fileMode os.FileMode = 0640 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	fileUploadStateUndef   fileUploadState = 0 | ||||||
|  | 	fileUploadStatePresent                 = 1 | ||||||
|  | 	fileUploadStateDeleted                 = 2 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func (t fileUploadState) String() string { | ||||||
|  | 	switch t { | ||||||
|  | 	case fileUploadStateUndef: | ||||||
|  | 		return "unknown" | ||||||
|  | 	case fileUploadStatePresent: | ||||||
|  | 		return "present" | ||||||
|  | 	case fileUploadStateDeleted: | ||||||
|  | 		return "deleted" | ||||||
|  | 	default: | ||||||
|  | 		return "invalid" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func OpenFileStore(path string) error { | ||||||
|  | 	if path == "" { | ||||||
|  | 		return errors.New("file-store not set") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Try to create the file store directory if it does not yet exist | ||||||
|  | 	if err := os.MkdirAll(path, dirMode); err != nil { | ||||||
|  | 		return errors.Wrap(err, "creating file store directory") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fileStoreDir = path[:] | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newFileUpload(tx *bolt.Tx, r io.Reader, fileName string, contentType string) (*fileUpload, error) { | ||||||
|  | 	id, err := uuid.NewRandom() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.Wrap(err, "generating UUID") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	filePath := fileStorePath(id, fileName) | ||||||
|  | 	if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil { | ||||||
|  | 		return nil, errors.Wrap(err, "creating file dir") | ||||||
|  | 	} | ||||||
|  | 	file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, fileMode) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.Wrap(err, "opening file") | ||||||
|  | 	} | ||||||
|  | 	defer file.Close() | ||||||
|  | 
 | ||||||
|  | 	hash := crc32.New(checksumTable) | ||||||
|  | 	tee := io.TeeReader(r, hash) | ||||||
|  | 	_, err = io.Copy(file, tee) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, errors.Wrap(err, "writing to file") | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	fu := &fileUpload{ | ||||||
|  | 		State:       fileUploadStatePresent, | ||||||
|  | 		ID:          id, | ||||||
|  | 		FileName:    fileName, | ||||||
|  | 		ContentType: contentType, | ||||||
|  | 		Checksum:    hash.Sum32(), | ||||||
|  | 	} | ||||||
|  | 	if err := fu.save(tx); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 	return fu, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func getFileUpload(tx *bolt.Tx, id uuid.UUID) (*fileUpload, error) { | ||||||
|  | 	bucket := tx.Bucket([]byte(BUCKET_FILE_UPLOAD)) | ||||||
|  | 	if bucket == nil { | ||||||
|  | 		return nil, errors.Errorf("bucket %v does not exist", BUCKET_FILE_UPLOAD) | ||||||
|  | 	} | ||||||
|  | 	storedBytes := bucket.Get(id[:]) | ||||||
|  | 	if storedBytes == nil { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 	fu := &fileUpload{} | ||||||
|  | 	err := Unmarshal(storedBytes, fu) | ||||||
|  | 	return fu, err | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (fu *fileUpload) save(tx *bolt.Tx) error { | ||||||
|  | 	bucket := tx.Bucket([]byte(BUCKET_FILE_UPLOAD)) | ||||||
|  | 	if bucket == nil { | ||||||
|  | 		return errors.Errorf("bucket %v does not exist", BUCKET_FILE_UPLOAD) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	buf, err := Marshal(fu) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return errors.Wrap(err, "encoding for database failed") | ||||||
|  | 	} | ||||||
|  | 	if err := bucket.Put(fu.ID[:], buf); err != nil { | ||||||
|  | 		return errors.Wrap(err, "database transaction failed") | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (fu *fileUpload) delete(tx *bolt.Tx) error { | ||||||
|  | 	// Remove the file in the backend | ||||||
|  | 	filePath := fileStorePath(fu.ID, fu.FileName) | ||||||
|  | 	if err := os.Remove(filePath); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Update the file in the server | ||||||
|  | 	if err := (&fileUpload{ | ||||||
|  | 		ID:    fu.ID, | ||||||
|  | 		State: fileUploadStateDeleted, | ||||||
|  | 	}).save(tx); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// Cleanup the parent directory | ||||||
|  | 	wrap := "deletion succeeded, but removing the file directory has failed" | ||||||
|  | 	return errors.Wrap(os.Remove(path.Dir(filePath)), wrap) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (fu *fileUpload) url() *url.URL { | ||||||
|  | 	rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName | ||||||
|  | 	urlParse, err := url.Parse(rawurl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic("could not construct /uploads/ url") | ||||||
|  | 	} | ||||||
|  | 	return urlParse | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func fileStorePath(id uuid.UUID, fileName string) string { | ||||||
|  | 	if fileStoreDir == "" { | ||||||
|  | 		panic("fileStoreDir called while the file store path has not been set") | ||||||
|  | 	} | ||||||
|  | 	return path.Join(fileStoreDir, hex.EncodeToString(id[:]), fileName) | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -3,6 +3,7 @@ module gitea.hashru.nl/dsprenkels/rushlink | |||||||
| go 1.12 | go 1.12 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
|  | 	github.com/google/uuid v1.1.1 | ||||||
| 	github.com/gorilla/mux v1.7.3 | 	github.com/gorilla/mux v1.7.3 | ||||||
| 	github.com/pkg/errors v0.8.1 | 	github.com/pkg/errors v0.8.1 | ||||||
| 	github.com/prometheus/client_golang v1.1.0 | 	github.com/prometheus/client_golang v1.1.0 | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -17,6 +17,8 @@ github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs | |||||||
| github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= | ||||||
| github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= | ||||||
| github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= | ||||||
|  | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= | ||||||
|  | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= | ||||||
| github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= | github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw= | ||||||
| github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= | ||||||
| github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= | ||||||
|  | |||||||
							
								
								
									
										226
									
								
								handlers.go
									
									
									
									
									
								
							
							
						
						
									
										226
									
								
								handlers.go
									
									
									
									
									
								
							| @ -4,11 +4,15 @@ import ( | |||||||
| 	"crypto/subtle" | 	"crypto/subtle" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"io" | ||||||
| 	"log" | 	"log" | ||||||
|  | 	"mime/multipart" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  | 	"os" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
| 	"github.com/gorilla/mux" | 	"github.com/gorilla/mux" | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
| 	bolt "go.etcd.io/bbolt" | 	bolt "go.etcd.io/bbolt" | ||||||
| @ -31,36 +35,48 @@ var ReservedPasteKeys = []string{"xd42", "example"} | |||||||
| var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" | var base64Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" | ||||||
| var base64Encoder = base64.RawURLEncoding.WithPadding(base64.NoPadding) | var base64Encoder = base64.RawURLEncoding.WithPadding(base64.NoPadding) | ||||||
| 
 | 
 | ||||||
| func (t pasteType) String() string { |  | ||||||
| 	switch t { |  | ||||||
| 	case typeUndef: |  | ||||||
| 		return "unknown" |  | ||||||
| 	case typePaste: |  | ||||||
| 		return "paste" |  | ||||||
| 	case typeRedirect: |  | ||||||
| 		return "redirect" |  | ||||||
| 	default: |  | ||||||
| 		return "invalid" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (t pasteState) String() string { |  | ||||||
| 	switch t { |  | ||||||
| 	case stateUndef: |  | ||||||
| 		return "unknown" |  | ||||||
| 	case statePresent: |  | ||||||
| 		return "present" |  | ||||||
| 	case stateDeleted: |  | ||||||
| 		return "deleted" |  | ||||||
| 	default: |  | ||||||
| 		return "invalid" |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func indexGetHandler(w http.ResponseWriter, r *http.Request) { | func indexGetHandler(w http.ResponseWriter, r *http.Request) { | ||||||
| 	render(w, r, "index", map[string]interface{}{}) | 	render(w, r, "index", map[string]interface{}{}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func uploadFileGetHandler(w http.ResponseWriter, r *http.Request) { | ||||||
|  | 	vars := mux.Vars(r) | ||||||
|  | 	id := vars["id"] | ||||||
|  | 
 | ||||||
|  | 	var fu *fileUpload | ||||||
|  | 	var badID bool | ||||||
|  | 	if err := DB.View(func(tx *bolt.Tx) error { | ||||||
|  | 		fuID, err := uuid.Parse(id) | ||||||
|  | 		if err != nil { | ||||||
|  | 			badID = true | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		fu, err = getFileUpload(tx, fuID) | ||||||
|  | 		return err | ||||||
|  | 	}); err != nil { | ||||||
|  | 		if badID { | ||||||
|  | 			renderError(w, r, http.StatusNotFound, "malformed file id") | ||||||
|  | 			return | ||||||
|  | 		} else { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	filePath := fileStorePath(fu.ID, fu.FileName) | ||||||
|  | 	file, err := os.Open(filePath) | ||||||
|  | 	if err != nil { | ||||||
|  | 		if os.IsNotExist(err) { | ||||||
|  | 			log.Printf("error: %v should exist according to the database, but it doesn't", filePath) | ||||||
|  | 			renderError(w, r, http.StatusNotFound, "file not found") | ||||||
|  | 			return | ||||||
|  | 		} else { | ||||||
|  | 			panic(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	w.Header().Set("Content-Type", fu.ContentType) | ||||||
|  | 	io.Copy(w, file) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func viewPasteHandler(w http.ResponseWriter, r *http.Request) { | func viewPasteHandler(w http.ResponseWriter, r *http.Request) { | ||||||
| 	viewPasteHandlerInner(w, r, 0) | 	viewPasteHandlerInner(w, r, 0) | ||||||
| } | } | ||||||
| @ -77,14 +93,28 @@ func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPas | |||||||
| 	vars := mux.Vars(r) | 	vars := mux.Vars(r) | ||||||
| 	key := vars["key"] | 	key := vars["key"] | ||||||
| 	var p *paste | 	var p *paste | ||||||
|  | 	var fuID *uuid.UUID | ||||||
|  | 	var fu *fileUpload | ||||||
| 	if err := DB.View(func(tx *bolt.Tx) error { | 	if err := DB.View(func(tx *bolt.Tx) error { | ||||||
| 		var err error | 		var err error | ||||||
| 		p, err = getPaste(tx, key) | 		p, err = getPaste(tx, key) | ||||||
|  | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
|  | 		} | ||||||
|  | 		if p != nil && p.Type == pasteTypeFileUpload { | ||||||
|  | 			var id uuid.UUID | ||||||
|  | 			copy(id[:], p.Content) | ||||||
|  | 			fuID = &id | ||||||
|  | 			fu, err = getFileUpload(tx, id) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		panic(err) | 		panic(err) | ||||||
| 		return |  | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
| 	if p == nil { | 	if p == nil { | ||||||
| 		renderError(w, r, http.StatusNotFound, "url key not found in the database") | 		renderError(w, r, http.StatusNotFound, "url key not found in the database") | ||||||
| 		return | 		return | ||||||
| @ -116,48 +146,88 @@ func viewPasteHandlerInner(w http.ResponseWriter, r *http.Request, flags viewPas | |||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	switch p.State { | 	switch p.State { | ||||||
| 	case statePresent: | 	case pasteStatePresent: | ||||||
|  | 		var location string | ||||||
|  | 		switch p.Type { | ||||||
|  | 		case pasteTypeFileUpload: | ||||||
|  | 			if fu == nil { | ||||||
|  | 				panic(fmt.Sprintf("file for id %v does not exist in database\n", fuID)) | ||||||
|  | 			} | ||||||
|  | 			location = fu.url().String() | ||||||
|  | 			break | ||||||
|  | 		case pasteTypeRedirect: | ||||||
|  | 			location = p.redirectURL().String() | ||||||
|  | 			break | ||||||
|  | 		default: | ||||||
|  | 			panic("paste type unsupported") | ||||||
|  | 		} | ||||||
| 		if flags&viewNoRedirect == 0 { | 		if flags&viewNoRedirect == 0 { | ||||||
| 			rawurl := string(p.Content) | 			http.Redirect(w, r, location, http.StatusSeeOther) | ||||||
| 			urlParse, err := url.Parse(rawurl) |  | ||||||
| 			if err != nil { |  | ||||||
| 				panic(errors.Wrapf(err, "invalid URL ('%v') in database for key '%v'", rawurl, p.Key)) |  | ||||||
| 		} | 		} | ||||||
| 			http.Redirect(w, r, urlParse.String(), http.StatusSeeOther) | 		fmt.Fprint(w, location) | ||||||
| 		} | 	case pasteStateDeleted: | ||||||
| 		w.Write(p.Content) | 		renderError(w, r, http.StatusGone, "paste has been deleted\n") | ||||||
| 	case stateDeleted: |  | ||||||
| 		renderError(w, r, http.StatusGone, "paste has been deleted") |  | ||||||
| 	default: | 	default: | ||||||
| 		panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key)) | 		panic(errors.Errorf("invalid paste.State (%v) for key '%v'", p.State, p.Key)) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func newPasteHandler(w http.ResponseWriter, r *http.Request) { | func newPasteHandler(w http.ResponseWriter, r *http.Request) { | ||||||
| 	if err := r.ParseMultipartForm(50 * 1000 * 1000); err != nil { | 	file, fileHeader, err := r.FormFile("file") | ||||||
| 		panic(err) | 	if err == nil { | ||||||
| 	} | 		newFileUploadPasteHandler(w, r, file, *fileHeader) | ||||||
| 
 | 		return | ||||||
| 	// Determine what kind of post this is, currently only `shorten=...` | 	} else if err == http.ErrMissingFile { | ||||||
| 	if len(r.PostForm) == 0 { | 		// Fallthrough | ||||||
| 		renderError(w, r, http.StatusBadRequest, "empty body in POST request\n") | 	} else { | ||||||
|  | 		msg := fmt.Sprintf("could not parse form: %v\n", err) | ||||||
|  | 		renderError(w, r, http.StatusBadRequest, msg) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	shorten_values, prs := r.PostForm["shorten"] | 
 | ||||||
| 	if !prs { | 	shorten := r.FormValue("shorten") | ||||||
|  | 	if shorten != "" { | ||||||
|  | 		newRedirectPasteHandler(w, r, shorten) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	renderError(w, r, http.StatusBadRequest, "no 'file' and no 'shorten' fields given in form\n") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newFileUploadPasteHandler(w http.ResponseWriter, r *http.Request, file multipart.File, header multipart.FileHeader) { | ||||||
|  | 	var fu *fileUpload | ||||||
|  | 	var paste *paste | ||||||
|  | 	if err := DB.Update(func(tx *bolt.Tx) error { | ||||||
|  | 		var err error | ||||||
|  | 		// Create the fileUpload in the database | ||||||
|  | 		fu, err = newFileUpload(tx, file, header.Filename, header.Header.Get("Content-Type")) | ||||||
|  | 		if err != nil { | ||||||
|  | 			panic(errors.Wrap(err, "creating fileUpload")) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		paste, err = shortenFileUploadID(tx, fu.ID) | ||||||
|  | 		return err | ||||||
|  | 	}); err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | 	data := map[string]interface{}{"Paste": paste} | ||||||
|  | 	render(w, r, "newFileUploadPasteSuccess", data) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func newPasteHandlerURLEncoded(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { | ||||||
|  | 	if err := r.ParseForm(); err != nil { | ||||||
|  | 		next(w, r) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  | 	shorten := r.PostFormValue("shorten") | ||||||
|  | 	if shorten == "" { | ||||||
| 		renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n") | 		renderError(w, r, http.StatusBadRequest, "no 'shorten' param given\n") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if len(shorten_values) != 1 { | 	newRedirectPasteHandler(w, r, shorten) | ||||||
| 		renderError(w, r, http.StatusBadRequest, "only one 'shorten' param is allowed per request\n") |  | ||||||
| 		return |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 	newRedirectPasteHandler(w, r) | func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request, rawurl string) { | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request) { |  | ||||||
| 	rawurl := r.PostForm.Get("shorten") |  | ||||||
| 	userURL, err := url.ParseRequestURI(rawurl) | 	userURL, err := url.ParseRequestURI(rawurl) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) | 		msg := fmt.Sprintf("invalid url (%v): %v", err, rawurl) | ||||||
| @ -165,17 +235,16 @@ func newRedirectPasteHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if userURL.Scheme == "" { | 	if userURL.Scheme == "" { | ||||||
| 		renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)") | 		renderError(w, r, http.StatusBadRequest, "invalid url (unspecified scheme)\n") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	if userURL.Host == "" { | 	if userURL.Host == "" { | ||||||
| 		renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)") | 		renderError(w, r, http.StatusBadRequest, "invalid url (unspecified host)\n") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var paste *paste | 	var paste *paste | ||||||
| 	if err := DB.Update(func(tx *bolt.Tx) error { | 	if err := DB.Update(func(tx *bolt.Tx) error { | ||||||
| 		// Generate a new delete token for this paste |  | ||||||
| 		var err error | 		var err error | ||||||
| 		paste, err = shortenURL(tx, userURL) | 		paste, err = shortenURL(tx, userURL) | ||||||
| 		return err | 		return err | ||||||
| @ -193,33 +262,60 @@ func deletePasteHandler(w http.ResponseWriter, r *http.Request) { | |||||||
| 
 | 
 | ||||||
| 	deleteToken := getDeleteTokenFromRequest(r) | 	deleteToken := getDeleteTokenFromRequest(r) | ||||||
| 	if deleteToken == "" { | 	if deleteToken == "" { | ||||||
| 		renderError(w, r, http.StatusBadRequest, "no delete token provided") | 		renderError(w, r, http.StatusBadRequest, "no delete token provided\n") | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	var errorCode int | 	var errorCode int | ||||||
|  | 	var paste paste | ||||||
| 	if err := DB.Update(func(tx *bolt.Tx) error { | 	if err := DB.Update(func(tx *bolt.Tx) error { | ||||||
| 		p, err := getPaste(tx, key) | 		p, err := getPaste(tx, key) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			errorCode = http.StatusNotFound | 			errorCode = http.StatusNotFound | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 		if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 1 { | 		if p.State == pasteStateDeleted { | ||||||
| 			p.delete(tx) | 			errorCode = http.StatusGone | ||||||
|  | 			return errors.New("already deleted") | ||||||
| 		} | 		} | ||||||
|  | 		if subtle.ConstantTimeCompare([]byte(deleteToken), []byte(p.DeleteToken)) == 0 { | ||||||
| 			errorCode = http.StatusForbidden | 			errorCode = http.StatusForbidden | ||||||
| 			return errors.New("invalid delete token") | 			return errors.New("invalid delete token") | ||||||
|  | 		} | ||||||
|  | 		if err := p.delete(tx); err != nil { | ||||||
|  | 			errorCode = http.StatusInternalServerError | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 		paste = *p | ||||||
|  | 		return nil | ||||||
| 	}); err != nil { | 	}); err != nil { | ||||||
| 		log.Printf("error: %v\n", err) | 		log.Printf("error: %v\n", err) | ||||||
| 		renderError(w, r, errorCode, fmt.Sprintf("error: %v", err)) | 		renderError(w, r, errorCode, fmt.Sprintf("error: %v\n", err)) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	data := map[string]interface{}{"Paste": paste} | ||||||
|  | 	render(w, r, "deletePasteSuccess", data) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Add a new fileUpload redirect to the database | ||||||
|  | // | ||||||
|  | // Returns the new paste key if the fileUpload was successfully added to the | ||||||
|  | // database | ||||||
|  | func shortenFileUploadID(tx *bolt.Tx, id uuid.UUID) (*paste, error) { | ||||||
|  | 	return shorten(tx, pasteTypeFileUpload, id[:]) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Add a new URL to the database | // Add a new URL to the database | ||||||
| // | // | ||||||
| // Returns the new ID if the url was successfully shortened | // Returns the new paste key if the url was successfully shortened | ||||||
| func shortenURL(tx *bolt.Tx, userURL *url.URL) (*paste, error) { | func shortenURL(tx *bolt.Tx, userURL *url.URL) (*paste, error) { | ||||||
|  | 	return shorten(tx, pasteTypeRedirect, []byte(userURL.String())) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Add a paste (of any kind) to the database with arbitrary content. | ||||||
|  | func shorten(tx *bolt.Tx, ty pasteType, content []byte) (*paste, error) { | ||||||
|  | 	// Generate the paste key | ||||||
| 	pasteKey, err := generatePasteKey(tx) | 	pasteKey, err := generatePasteKey(tx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, errors.Wrap(err, "generating paste key") | 		return nil, errors.Wrap(err, "generating paste key") | ||||||
| @ -233,9 +329,9 @@ func shortenURL(tx *bolt.Tx, userURL *url.URL) (*paste, error) { | |||||||
| 
 | 
 | ||||||
| 	// Store the new key | 	// Store the new key | ||||||
| 	p := paste{ | 	p := paste{ | ||||||
| 		Type:        typeRedirect, | 		Type:        ty, | ||||||
| 		State:       statePresent, | 		State:       pasteStatePresent, | ||||||
| 		Content:     []byte(userURL.String()), | 		Content:     content, | ||||||
| 		Key:         pasteKey, | 		Key:         pasteKey, | ||||||
| 		DeleteToken: deleteToken, | 		DeleteToken: deleteToken, | ||||||
| 		TimeCreated: time.Now().UTC(), | 		TimeCreated: time.Now().UTC(), | ||||||
|  | |||||||
							
								
								
									
										93
									
								
								paste.go
									
									
									
									
									
								
							
							
						
						
									
										93
									
								
								paste.go
									
									
									
									
									
								
							| @ -3,9 +3,11 @@ package rushlink | |||||||
| import ( | import ( | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
|  | 	"net/url" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"github.com/google/uuid" | ||||||
| 	"github.com/pkg/errors" | 	"github.com/pkg/errors" | ||||||
| 	bolt "go.etcd.io/bbolt" | 	bolt "go.etcd.io/bbolt" | ||||||
| ) | ) | ||||||
| @ -22,18 +24,52 @@ type paste struct { | |||||||
| 	TimeCreated time.Time | 	TimeCreated time.Time | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Note: we use iota here. That means removals of pasteType* are not allowed, | ||||||
|  | // because this changes the value of the constant. Please add the comment | ||||||
|  | |||||||
|  | // "// deprecated" if you want to remove the constant. Additions are only | ||||||
|  | // allowed at the bottom of this block, for the same reason. | ||||||
| const ( | const ( | ||||||
| 	typeUndef    pasteType = 0 | 	pasteTypeUndef pasteType = iota | ||||||
| 	typePaste              = 1 | 	pasteTypePaste | ||||||
| 	typeRedirect           = 2 | 	pasteTypeRedirect | ||||||
|  | 	pasteTypeFileUpload | ||||||
| 
				
					
						mrngm
						commented  The type of the other states is not  The type of the other states is not `pasteState`, see https://play.golang.org/p/lVjTWBgRQU- | |||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | // Note: we use iota here. See the comment above pasteType* | ||||||
| const ( | const ( | ||||||
| 	stateUndef   pasteState = 0 | 	pasteStateUndef pasteState = iota | ||||||
| 	statePresent            = 1 | 	pasteStatePresent | ||||||
| 	stateDeleted            = 2 | 	pasteStateDeleted | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | func (t pasteType) String() string { | ||||||
|  | 	switch t { | ||||||
|  | 	case pasteTypeUndef: | ||||||
|  | 		return "unknown" | ||||||
|  | 	case pasteTypePaste: | ||||||
|  | 		return "paste" | ||||||
|  | 	case pasteTypeRedirect: | ||||||
|  | 		return "redirect" | ||||||
|  | 	case pasteTypeFileUpload: | ||||||
|  | 		return "file" | ||||||
|  | 	default: | ||||||
|  | 		return "invalid" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (t pasteState) String() string { | ||||||
|  | 	switch t { | ||||||
|  | 	case pasteStateUndef: | ||||||
|  | 		return "unknown" | ||||||
|  | 	case pasteStatePresent: | ||||||
|  | 		return "present" | ||||||
|  | 	case pasteStateDeleted: | ||||||
|  | 		return "deleted" | ||||||
|  | 	default: | ||||||
|  | 		return "invalid" | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // Retrieve a paste from the database | // Retrieve a paste from the database | ||||||
| func getPaste(tx *bolt.Tx, key string) (*paste, error) { | func getPaste(tx *bolt.Tx, key string) (*paste, error) { | ||||||
| 	pastesBucket := tx.Bucket([]byte(BUCKET_PASTES)) | 	pastesBucket := tx.Bucket([]byte(BUCKET_PASTES)) | ||||||
| @ -65,13 +101,46 @@ func (p *paste) save(tx *bolt.Tx) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (p paste) delete(tx *bolt.Tx) error { | func (p *paste) delete(tx *bolt.Tx) error { | ||||||
|  | 	// Remove the (maybe) attached file | ||||||
|  | 	if p.Type == pasteTypeFileUpload { | ||||||
|  | 		fuID, err := uuid.FromBytes(p.Content) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errors.Wrap(err, "failed to parse uuid") | ||||||
|  | 		} | ||||||
|  | 		fu, err := getFileUpload(tx, fuID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return errors.Wrap(err, "failed to find file in database") | ||||||
|  | 		} | ||||||
|  | 		if err := fu.delete(tx); err != nil { | ||||||
|  | 			return errors.Wrap(err, "failed to remove file") | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Replace the old paste with a new empty paste | 	// Replace the old paste with a new empty paste | ||||||
| 	return (&paste{ | 	p.Type = pasteTypeUndef | ||||||
| 		Key:         p.Key, | 	p.State = pasteStateDeleted | ||||||
| 		State:       stateDeleted, | 	p.Content = []byte{} | ||||||
| 		DeleteToken: p.DeleteToken, | 	if err := p.save(tx); err != nil { | ||||||
| 	}).save(tx) | 		return errors.Wrap(err, "failed to delete paste in database") | ||||||
|  | 	} | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Get the URL from this paste. | ||||||
|  | // | ||||||
|  | // This function assumes that the paste is valid.  If the paste struct is | ||||||
|  | // corrupted in some way, this function will panic. | ||||||
|  | func (p *paste) redirectURL() *url.URL { | ||||||
|  | 	if p.Type != pasteTypeRedirect { | ||||||
|  | 		panic("expected p.Type to be pasteTypeRedirect") | ||||||
|  | 	} | ||||||
|  | 	rawurl := string(p.Content) | ||||||
|  | 	urlParse, err := url.Parse(rawurl) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(errors.Wrapf(err, "invalid URL ('%v') in database for key '%v'", rawurl, p.Key)) | ||||||
|  | 	} | ||||||
|  | 	return urlParse | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Generate a key until it is not in the database, this occurs in O(log N), | // Generate a key until it is not in the database, this occurs in O(log N), | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"log" | 	"log" | ||||||
| 	"net/http" | 	"net/http" | ||||||
|  | 	"runtime/debug" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
| 	"github.com/gorilla/mux" | 	"github.com/gorilla/mux" | ||||||
| @ -15,12 +16,14 @@ func recoveryMiddleware(next http.Handler) http.Handler { | |||||||
| 			defer func() { | 			defer func() { | ||||||
| 				if err := recover(); err != nil { | 				if err := recover(); err != nil { | ||||||
| 					log.Printf("error: panic while recovering from another panic: %v\n", err) | 					log.Printf("error: panic while recovering from another panic: %v\n", err) | ||||||
|  | 					debug.PrintStack() | ||||||
| 					fmt.Fprintf(w, "internal server error: %v\n", err) | 					fmt.Fprintf(w, "internal server error: %v\n", err) | ||||||
| 				} | 				} | ||||||
| 			}() | 			}() | ||||||
| 
 | 
 | ||||||
| 			if err := recover(); err != nil { | 			if err := recover(); err != nil { | ||||||
| 				log.Printf("error: %v\n", err) | 				log.Printf("error: %v\n", err) | ||||||
|  | 				debug.PrintStack() | ||||||
| 				renderInternalServerError(w, r, err) | 				renderInternalServerError(w, r, err) | ||||||
| 			} | 			} | ||||||
| 		}() | 		}() | ||||||
| @ -39,6 +42,7 @@ func StartMainServer(addr string) { | |||||||
| 	router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/meta", viewPasteHandlerMeta).Methods("GET") | 	router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/meta", viewPasteHandlerMeta).Methods("GET") | ||||||
| 	router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", deletePasteHandler).Methods("DELETE") | 	router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}", deletePasteHandler).Methods("DELETE") | ||||||
| 	router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/delete", deletePasteHandler).Methods("POST") | 	router.HandleFunc("/{key:[A-Za-z0-9-_]{4,}}/delete", deletePasteHandler).Methods("POST") | ||||||
|  | 	router.HandleFunc("/uploads/{id:[A-Za-z0-9-_]+}/{filename:.+}", uploadFileGetHandler).Methods("GET") | ||||||
| 
 | 
 | ||||||
| 	srv := &http.Server{ | 	srv := &http.Server{ | ||||||
| 		Handler:      router, | 		Handler:      router, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	
See below comment