├── LICENSE ├── README.md └── main.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Fabian Lindfors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fdb-object-store 2 | 3 | A simple object store built on top of [FoundationDB](https://www.foundationdb.org). This is only a proof of concept developed for a blog post: [Building an object store with FoundationDB](https://fabianlindfors.se/blog/building-an-object-store-with-foundation-db/). 4 | 5 | ## Running 6 | 7 | Requires the FoundationDB Go bindings, follow the instructions [here](https://github.com/apple/foundationdb/tree/master/bindings/go) to install. Also requires the [Gin web framework](https://github.com/gin-gonic/gin). After dependencies have been installed the server can be started with `go run main.go`. 8 | 9 | ## Using 10 | 11 | The object store is dead simple to use and only has two features, uploading files and downloading them. 12 | 13 | File uploads are handled by making a POST file upload to `/object/path/to/file.txt`. Everything after `/object/` will be used as the file name, there is no notion of paths or directories but they can be simulated with slashes. When uploading a content type must also be specified with the POST form field `content_type`. 14 | 15 | ``` 16 | # Example upload using HTTPie 17 | $ http -f POST localhost:8080/object/images/my_image.png content_type="image/png" file@local_file.png 18 | ``` 19 | 20 | Downloading an existing file is as simple as making a GET request to the same past as the upload. This can also be done through a browser. 21 | 22 | ``` 23 | # Example download of previously uploaded file (with HTTPie) 24 | $ http -d GET localhost:8080/object/images/my_image.png 25 | ``` 26 | 27 | ## License 28 | 29 | All code is licensed under [MIT](https://github.com/Fabianlindfors/fdb-object-store/blob/master/LICENSE). -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "github.com/apple/foundationdb/bindings/go/src/fdb" 6 | "github.com/apple/foundationdb/bindings/go/src/fdb/tuple" 7 | "github.com/gin-gonic/gin" 8 | "io" 9 | "strings" 10 | ) 11 | 12 | var db fdb.Database 13 | 14 | type File struct { 15 | Data []byte 16 | ContentType string 17 | } 18 | 19 | func getFile(name string) *File { 20 | file, _ := db.Transact(func(tr fdb.Transaction) (interface{}, error) { 21 | contentType := tr.Get(tuple.Tuple{name, "content-type"}).MustGet() 22 | if contentType == nil { 23 | return nil, nil 24 | } 25 | 26 | // Retrieve the split data using a prefix query 27 | start, end := tuple.Tuple{name, "data"}.FDBRangeKeys() 28 | r := fdb.KeyRange{Begin: start, End: end} 29 | kvSlice := tr.GetRange(r, fdb.RangeOptions{}).GetSliceOrPanic() 30 | 31 | // Combine the retrieved file data into a buffer 32 | var b bytes.Buffer 33 | for _, kv := range kvSlice { 34 | b.Write(kv.Value) 35 | } 36 | 37 | return &File{Data: b.Bytes(), ContentType: string(contentType)}, nil 38 | }) 39 | 40 | if file == nil { 41 | return nil 42 | } 43 | 44 | return file.(*File) 45 | } 46 | 47 | func saveFile(name string, contentType string, reader io.Reader) { 48 | db.Transact(func(tr fdb.Transaction) (ret interface{}, e error) { 49 | buffer := make([]byte, 10000) 50 | i := 0 51 | 52 | for { 53 | n, err := reader.Read(buffer) 54 | 55 | if err == io.EOF { 56 | break 57 | } 58 | 59 | tr.Set(tuple.Tuple{name, "data", i}, buffer[:n]) 60 | i++ 61 | } 62 | 63 | tr.Set(tuple.Tuple{name, "content-type"}, []byte(contentType)) 64 | 65 | return 66 | }) 67 | } 68 | 69 | func main() { 70 | fdb.MustAPIVersion(510) 71 | db = fdb.MustOpenDefault() 72 | 73 | router := gin.Default() 74 | 75 | router.GET("/object/*name", func(c *gin.Context) { 76 | name := c.Param("name") 77 | file := getFile(name) 78 | 79 | // File not found in FDB 80 | if file == nil { 81 | c.AbortWithStatus(404) 82 | return 83 | } 84 | 85 | // Split file path by slash to get file name 86 | splitName := strings.Split(name, "/") 87 | 88 | c.Header("Content-Description", "File Transfer") 89 | c.Header("Content-Disposition", "attachment; filename="+splitName[len(splitName)-1]) 90 | c.Data(200, file.ContentType, file.Data) 91 | }) 92 | 93 | router.POST("/object/*name", func(c *gin.Context) { 94 | name := c.Param("name") 95 | 96 | // Content type will be needed to enable downloads later 97 | contentType := c.PostForm("content_type") 98 | file, err := c.FormFile("file") 99 | if err != nil { 100 | c.AbortWithError(400, err) 101 | return 102 | } 103 | 104 | reader, _ := file.Open() 105 | defer reader.Close() // Make sure to close the file handle 106 | 107 | saveFile(name, contentType, reader) 108 | 109 | c.String(200, "File saved") 110 | }) 111 | 112 | router.Run() 113 | } 114 | --------------------------------------------------------------------------------