├── .travis.yml ├── LICENSE.md ├── README.md ├── config.go ├── imstor.go ├── imstor_suite_test.go ├── imstor_test.go ├── jpeg_format.go ├── mocks_test.go ├── png2jpeg.go ├── resizer.go ├── store.go └── writing.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Deiwin Sarjas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Imstor 2 | A Golang image storage engine. Used to create and store different sizes/thumbnails of user uploaded images. 3 | 4 | [![Build Status](https://travis-ci.org/deiwin/imstor.svg?branch=master)](https://travis-ci.org/deiwin/imstor) 5 | [![Coverage](http://gocover.io/_badge/github.com/deiwin/imstor?0)](http://gocover.io/github.com/deiwin/imstor) 6 | [![GoDoc](https://godoc.org/github.com/deiwin/imstor?status.svg)](https://godoc.org/github.com/deiwin/imstor) 7 | 8 | ## Description 9 | 10 | **Imstor** enables you to create copies (or thumbnails) of your images and stores 11 | them along with the original image on your filesystem. The image and its 12 | copies are stored in a file structure based on the (zero-prefixed, decimal) 13 | CRC 64 checksum of the original image. The last 2 characters of the checksum 14 | are used as the lvl 1 directory name. 15 | 16 | **Imstor** supports any image format you can decode to go's own image.Image 17 | and then back to your preferred format. The decoder for any given image is 18 | chosen by the image's mimetype. 19 | 20 | ### Example folder structure 21 | Folder name and contents, given the checksum `08446744073709551615` and 22 | sizes named "*small*" and "*large*": 23 | ``` 24 | /configured/root/path/15/08446744073709551615/original.jpg 25 | /configured/root/path/15/08446744073709551615/small.jpg 26 | /configured/root/path/15/08446744073709551615/large.jpg 27 | ``` 28 | 29 | ## Usage 30 | See tests for usage examples. 31 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package imstor 2 | 3 | import ( 4 | "image" 5 | "io" 6 | 7 | "github.com/deiwin/gonfigure" 8 | ) 9 | 10 | var ( 11 | rootPathEnvProperty = gonfigure.NewRequiredEnvProperty("IMSTOR_ROOT_PATH") 12 | ) 13 | 14 | type Config struct { 15 | RootPath string 16 | CopySizes []Size 17 | Formats []Format 18 | } 19 | 20 | func NewConfig(copySizes []Size, formats []Format) *Config { 21 | return &Config{ 22 | RootPath: rootPathEnvProperty.Value(), 23 | CopySizes: copySizes, 24 | Formats: formats, 25 | } 26 | } 27 | 28 | // Size specifies a set of dimensions and a name that a copy of an image will 29 | // be stored as 30 | type Size struct { 31 | Name string 32 | Height uint 33 | Width uint 34 | } 35 | 36 | // A Format describes how an image of a certaing mimetype can be decoded and 37 | // then encoded. 38 | type Format interface { 39 | DecodableMediaType() string 40 | Decode(io.Reader) (image.Image, error) 41 | Encode(io.Writer, image.Image) error 42 | EncodedExtension() string 43 | } 44 | -------------------------------------------------------------------------------- /imstor.go: -------------------------------------------------------------------------------- 1 | // Package imstor enables you to create copies (or thumbnails) of your images and stores 2 | // them along with the original image on your filesystem. The image and its 3 | // copies are are stored in a file structure based on the (zero-prefixed, decimal) 4 | // CRC 64 checksum of the original image. The last 2 characters of the checksum 5 | // are used as the lvl 1 directory name. 6 | // 7 | // Example folder name and contents, given the checksum 08446744073709551615 and 8 | // sizes named "small" and "large": 9 | // 10 | // /configured/root/path/15/08446744073709551615/original.jpeg 11 | // /configured/root/path/15/08446744073709551615/small.jpeg 12 | // /configured/root/path/15/08446744073709551615/large.jpeg 13 | package imstor 14 | 15 | import ( 16 | "errors" 17 | "fmt" 18 | "hash/crc64" 19 | "image" 20 | _ "image/jpeg" 21 | _ "image/png" 22 | "io/ioutil" 23 | "os" 24 | "path" 25 | "path/filepath" 26 | "strings" 27 | 28 | "github.com/vincent-petithory/dataurl" 29 | ) 30 | 31 | var crcTable = crc64.MakeTable(crc64.ISO) 32 | 33 | const ( 34 | originalImageName = "original" 35 | ) 36 | 37 | type storage struct { 38 | conf *Config 39 | resizer Resizer 40 | } 41 | 42 | // Storage is the engine that can be used to store images and retrieve their paths 43 | type Storage interface { 44 | Store(mediaType string, data []byte) error 45 | StoreDataURL(string) error 46 | Checksum([]byte) string 47 | ChecksumDataURL(string) (string, error) 48 | PathFor(checksum string) (string, error) 49 | PathForSize(checksum, size string) (string, error) 50 | HasSizesForChecksum(checksum string, sizes []string) (bool, error) 51 | GetSize(checksum, size string) (image.Image, error) 52 | } 53 | 54 | // New creates a storage engine using the default Resizer 55 | func New(conf *Config) Storage { 56 | return storage{ 57 | conf: conf, 58 | resizer: DefaultResizer, 59 | } 60 | } 61 | 62 | // NewWithCustomResizer creates a storage engine using a custom resizer 63 | func NewWithCustomResizer(conf *Config, resizer Resizer) Storage { 64 | return storage{ 65 | conf: conf, 66 | resizer: resizer, 67 | } 68 | } 69 | 70 | func getStructuredFolderPath(checksum string) string { 71 | lvl1Dir := checksum[len(checksum)-2:] 72 | return path.Join(lvl1Dir, checksum) 73 | } 74 | 75 | func (s storage) ChecksumDataURL(str string) (string, error) { 76 | dataURL, err := dataurl.DecodeString(str) 77 | if err != nil { 78 | return "", err 79 | } 80 | return s.Checksum(dataURL.Data), nil 81 | } 82 | 83 | func (s storage) Checksum(data []byte) string { 84 | crc := crc64.Checksum(data, crcTable) 85 | return fmt.Sprintf("%020d", crc) 86 | } 87 | 88 | func (s storage) PathFor(sum string) (string, error) { 89 | return s.PathForSize(sum, originalImageName) 90 | } 91 | 92 | func (s storage) PathForSize(sum, size string) (string, error) { 93 | dir := getStructuredFolderPath(sum) 94 | absDirPath := filepath.Join(s.conf.RootPath, filepath.FromSlash(dir)) 95 | files, err := ioutil.ReadDir(absDirPath) 96 | if err != nil { 97 | return "", err 98 | } 99 | for _, file := range files { 100 | if !file.IsDir() && hasNameWithoutExtension(file.Name(), size) { 101 | return filepath.Join(dir, file.Name()), nil 102 | } 103 | } 104 | return "", errors.New("File not found!") 105 | } 106 | 107 | func (s storage) HasSizesForChecksum(sum string, sizes []string) (bool, error) { 108 | dir := getStructuredFolderPath(sum) 109 | absDirPath := filepath.Join(s.conf.RootPath, filepath.FromSlash(dir)) 110 | files, err := ioutil.ReadDir(absDirPath) 111 | if os.IsNotExist(err) { 112 | return false, nil 113 | } else if err != nil { 114 | return false, err 115 | } 116 | LoopSizes: 117 | for _, size := range sizes { 118 | for _, file := range files { 119 | if !file.IsDir() && hasNameWithoutExtension(file.Name(), size) { 120 | continue LoopSizes 121 | } 122 | } 123 | return false, nil 124 | } 125 | return true, nil 126 | } 127 | 128 | func hasNameWithoutExtension(fileName, name string) bool { 129 | extension := path.Ext(fileName) 130 | nameWithoutExtension := strings.TrimSuffix(fileName, extension) 131 | return nameWithoutExtension == name 132 | } 133 | 134 | func (s storage) GetSize(sum, size string) (image.Image, error) { 135 | relPath, err := s.PathForSize(sum, size) 136 | if err != nil { 137 | return nil, err 138 | } 139 | absPath := filepath.Join(s.conf.RootPath, relPath) 140 | 141 | file, err := os.Open(absPath) 142 | if err != nil { 143 | return nil, err 144 | } 145 | defer file.Close() 146 | 147 | // TODO: currently relies on `_ "image/jpeg"` to be imported here. The Format type needs some rework 148 | // before it can be used to properly decode written files. (Say, having only a PNG2JPEG Format would not 149 | // work, because the Format doesn't know how to decode the jpeg file) 150 | image, _, err := image.Decode(file) 151 | return image, err 152 | } 153 | -------------------------------------------------------------------------------- /imstor_suite_test.go: -------------------------------------------------------------------------------- 1 | package imstor_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestImstor(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Imstor Suite") 13 | } 14 | -------------------------------------------------------------------------------- /imstor_test.go: -------------------------------------------------------------------------------- 1 | package imstor_test 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/deiwin/imstor" 11 | 12 | . "github.com/onsi/ginkgo" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var ( 17 | dataString = "somedata" 18 | data = []byte(dataString) 19 | checksum = "06343430109577305132" 20 | folderPath = "32/06343430109577305132" 21 | img = image.NewGray16(image.Rect(0, 0, 3, 3)) 22 | tempDir string 23 | sizes = []imstor.Size{ 24 | imstor.Size{ 25 | Name: "small", 26 | Height: 30, 27 | Width: 30, 28 | }, imstor.Size{ 29 | Name: "large", 30 | Height: 300, 31 | Width: 300, 32 | }, 33 | } 34 | formats = []imstor.Format{ 35 | png2JPEG{}, 36 | jpegFormat{}, 37 | } 38 | ) 39 | 40 | var _ = Describe("Imstor", func() { 41 | var s imstor.Storage 42 | BeforeEach(func() { 43 | var err error 44 | tempDir, err = ioutil.TempDir("", "imstor-test") 45 | Expect(err).NotTo(HaveOccurred()) 46 | conf := &imstor.Config{ 47 | RootPath: tempDir, 48 | CopySizes: sizes, 49 | Formats: formats, 50 | } 51 | s = imstor.NewWithCustomResizer(conf, mockResizer{}) 52 | }) 53 | 54 | AfterEach(func() { 55 | err := os.RemoveAll(tempDir) 56 | Expect(err).NotTo(HaveOccurred()) 57 | }) 58 | 59 | Describe("Checksum", func() { 60 | It("should return the checksum for given bytes", func() { 61 | c := s.Checksum(data) 62 | Expect(c).To(Equal(checksum)) 63 | }) 64 | 65 | It("should be able to get the checksm for data encoded as a data URL", func() { 66 | c, err := s.ChecksumDataURL(fmt.Sprintf("data:,%s", dataString)) 67 | Expect(err).NotTo(HaveOccurred()) 68 | Expect(c).To(Equal(checksum)) 69 | }) 70 | }) 71 | 72 | Describe("Store", func() { 73 | var expectImageFileToExist = func(name string) { 74 | path := filepath.Join(tempDir, filepath.FromSlash(folderPath), name) 75 | if _, err := os.Stat(path); os.IsNotExist(err) { 76 | Fail(fmt.Sprintf("Expected file '%s' to exist", path)) 77 | } 78 | } 79 | 80 | BeforeEach(func() { 81 | err := s.Store("image/jpeg", data) 82 | Expect(err).NotTo(HaveOccurred()) 83 | }) 84 | 85 | It("should create a image and copies", func() { 86 | expectImageFileToExist("original.jpg") 87 | expectImageFileToExist("small.jpg") 88 | expectImageFileToExist("large.jpg") 89 | // most assertions are in mock objects 90 | }) 91 | 92 | Context("with new configuration size added", func() { 93 | BeforeEach(func() { 94 | updatedSizes := append(sizes, imstor.Size{ 95 | Name: "newFormat", 96 | Height: 16, 97 | Width: 16, 98 | }) 99 | conf := &imstor.Config{ 100 | RootPath: tempDir, 101 | CopySizes: updatedSizes, 102 | Formats: formats, 103 | } 104 | s = imstor.NewWithCustomResizer(conf, mockResizer{}) 105 | }) 106 | 107 | Describe("storing the same image", func() { 108 | var err error 109 | BeforeEach(func() { 110 | err = s.Store("image/jpeg", data) 111 | }) 112 | 113 | It("should return without an error", func() { 114 | Expect(err).NotTo(HaveOccurred()) 115 | }) 116 | 117 | It("should still have the image and copies plus the new one", func() { 118 | expectImageFileToExist("original.jpg") 119 | expectImageFileToExist("small.jpg") 120 | expectImageFileToExist("large.jpg") 121 | expectImageFileToExist("newFormat.jpg") 122 | }) 123 | }) 124 | }) 125 | 126 | Describe("storing the same image", func() { 127 | var err error 128 | BeforeEach(func() { 129 | err = s.Store("image/jpeg", data) 130 | }) 131 | 132 | It("should return without an error", func() { 133 | Expect(err).NotTo(HaveOccurred()) 134 | }) 135 | 136 | It("should still have the image and copies", func() { 137 | expectImageFileToExist("original.jpg") 138 | expectImageFileToExist("small.jpg") 139 | expectImageFileToExist("large.jpg") 140 | }) 141 | }) 142 | 143 | It("should return proper path for the original image", func() { 144 | path, err := s.PathFor(checksum) 145 | Expect(err).NotTo(HaveOccurred()) 146 | Expect(path).To(Equal(filepath.Join(filepath.FromSlash(folderPath), "original.jpg"))) 147 | }) 148 | 149 | It("should return an error for an improper checksum", func() { 150 | _, err := s.PathFor("somethingrandom") 151 | Expect(err).To(HaveOccurred()) 152 | }) 153 | 154 | It("should return proper paths for different sizes", func() { 155 | path, err := s.PathForSize(checksum, "small") 156 | Expect(err).NotTo(HaveOccurred()) 157 | Expect(path).To(Equal(filepath.Join(filepath.FromSlash(folderPath), "small.jpg"))) 158 | 159 | path, err = s.PathForSize(checksum, "large") 160 | Expect(err).NotTo(HaveOccurred()) 161 | Expect(path).To(Equal(filepath.Join(filepath.FromSlash(folderPath), "large.jpg"))) 162 | }) 163 | 164 | Describe("HasSizesForChecksum", func() { 165 | It("should return true if the sizes exist for that checksum", func() { 166 | hasSizes, err := s.HasSizesForChecksum(checksum, []string{"small", "large"}) 167 | Expect(err).NotTo(HaveOccurred()) 168 | Expect(hasSizes).To(BeTrue()) 169 | }) 170 | 171 | It("should return false if any of the sizes don't exist for that checksum", func() { 172 | hasSizes, err := s.HasSizesForChecksum(checksum, []string{"smallish", "large"}) 173 | Expect(err).NotTo(HaveOccurred()) 174 | Expect(hasSizes).To(BeFalse()) 175 | }) 176 | 177 | It("should return false for a random checksum", func() { 178 | hasSizes, err := s.HasSizesForChecksum("arandomchecksum", []string{"small", "large"}) 179 | Expect(err).NotTo(HaveOccurred()) 180 | Expect(hasSizes).To(BeFalse()) 181 | }) 182 | }) 183 | 184 | It("should not return a path for improper size (say a prefix of an actual size)", func() { 185 | _, err := s.PathForSize(checksum, "smal") 186 | Expect(err).To(HaveOccurred()) 187 | }) 188 | }) 189 | }) 190 | -------------------------------------------------------------------------------- /jpeg_format.go: -------------------------------------------------------------------------------- 1 | package imstor 2 | 3 | import ( 4 | "image" 5 | "image/jpeg" 6 | "io" 7 | ) 8 | 9 | var jpegEncodingOptions = &jpeg.Options{ 10 | Quality: jpeg.DefaultQuality, 11 | } 12 | 13 | // JPEGFormat decodes a jpeg image and encodes it as a JPEG with the extension jpg 14 | var JPEGFormat Format = jpegFormat{} 15 | 16 | type jpegFormat struct { 17 | } 18 | 19 | func (f jpegFormat) Decode(r io.Reader) (image.Image, error) { 20 | return jpeg.Decode(r) 21 | } 22 | 23 | func (f jpegFormat) DecodableMediaType() string { 24 | return "image/jpeg" 25 | } 26 | 27 | func (f jpegFormat) Encode(w io.Writer, i image.Image) error { 28 | return jpeg.Encode(w, i, jpegEncodingOptions) 29 | } 30 | 31 | func (f jpegFormat) EncodedExtension() string { 32 | return "jpg" 33 | } 34 | -------------------------------------------------------------------------------- /mocks_test.go: -------------------------------------------------------------------------------- 1 | package imstor_test 2 | 3 | import ( 4 | "image" 5 | "io" 6 | "io/ioutil" 7 | "path/filepath" 8 | 9 | "github.com/deiwin/imstor" 10 | . "github.com/onsi/ginkgo" 11 | . "github.com/onsi/gomega" 12 | ) 13 | 14 | var smallImg = image.NewGray16(image.Rect(0, 0, 2, 2)) 15 | var largeImg = image.NewGray16(image.Rect(0, 0, 4, 4)) 16 | var newSizeImg = image.NewGray16(image.Rect(0, 0, 5, 5)) 17 | 18 | type png2JPEG struct { 19 | imstor.Format 20 | } 21 | 22 | func (f png2JPEG) DecodableMediaType() string { 23 | return "image/png" 24 | } 25 | 26 | type jpegFormat struct { 27 | imstor.Format 28 | } 29 | 30 | func (f jpegFormat) DecodableMediaType() string { 31 | return "image/jpeg" 32 | } 33 | 34 | func (f jpegFormat) Decode(r io.Reader) (image.Image, error) { 35 | bytes, err := ioutil.ReadAll(r) 36 | Expect(err).NotTo(HaveOccurred()) 37 | Expect(bytes).To(Equal(data)) 38 | return img, nil 39 | } 40 | 41 | func (f jpegFormat) EncodedExtension() string { 42 | return "jpg" 43 | } 44 | 45 | func (f jpegFormat) Encode(w io.Writer, i image.Image) error { 46 | if i == smallImg { 47 | expectToBeFile(w, "small.jpg") 48 | } else if i == largeImg { 49 | expectToBeFile(w, "large.jpg") 50 | } else if i == img { 51 | expectToBeFile(w, "original.jpg") 52 | } else if i == newSizeImg { 53 | expectToBeFile(w, "newFormat.jpg") 54 | } else { 55 | Fail("an unexpected image") 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func expectToBeFile(w io.Writer, name string) { 62 | w.Write(data) 63 | path := filepath.Join(tempDir, filepath.FromSlash(folderPath), name) 64 | fileContents, err := ioutil.ReadFile(path) 65 | Expect(err).NotTo(HaveOccurred()) 66 | Expect(fileContents).To(Equal(data)) 67 | } 68 | 69 | type mockResizer struct { 70 | imstor.Resizer 71 | } 72 | 73 | func (r mockResizer) Thumbnail(w, h uint, i image.Image) image.Image { 74 | Expect(i).To(Equal(img)) 75 | if w == 30 && h == 30 { 76 | return smallImg 77 | } else if w == 300 && h == 300 { 78 | return largeImg 79 | } else if w == 16 && h == 16 { 80 | return newSizeImg 81 | } 82 | Fail("unexpected size") 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /png2jpeg.go: -------------------------------------------------------------------------------- 1 | package imstor 2 | 3 | import ( 4 | "image" 5 | "image/jpeg" 6 | "image/png" 7 | "io" 8 | ) 9 | 10 | // PNG2JPEG format decodes an image from the PNG format and encodes it as a JPEG 11 | var PNG2JPEG Format = png2JPEG{} 12 | 13 | type png2JPEG struct { 14 | } 15 | 16 | func (f png2JPEG) Decode(r io.Reader) (image.Image, error) { 17 | return png.Decode(r) 18 | } 19 | 20 | func (f png2JPEG) DecodableMediaType() string { 21 | return "image/png" 22 | } 23 | 24 | func (f png2JPEG) Encode(w io.Writer, i image.Image) error { 25 | return jpeg.Encode(w, i, jpegEncodingOptions) 26 | } 27 | 28 | func (f png2JPEG) EncodedExtension() string { 29 | return "jpg" 30 | } 31 | -------------------------------------------------------------------------------- /resizer.go: -------------------------------------------------------------------------------- 1 | package imstor 2 | 3 | import ( 4 | "image" 5 | 6 | "github.com/nfnt/resize" 7 | ) 8 | 9 | // A Resizer can resize an image into the given dimensions 10 | type Resizer interface { 11 | // Resize should scale an image to new width and height. If one of the 12 | // parameters width or height is set to 0, its size will be calculated so that 13 | // the aspect ratio is that of the originating image. 14 | Resize(width, height uint, i image.Image) image.Image 15 | // Thumbnail should downscale provided image to max width and height preserving 16 | // original aspect ratio. It should return original image, without processing, 17 | // if original sizes are already smaller than the provided constraints. 18 | Thumbnail(maxWidth, maxHeight uint, i image.Image) image.Image 19 | } 20 | 21 | var DefaultResizer = defaultResizer{} 22 | 23 | type defaultResizer struct{} 24 | 25 | func (r defaultResizer) Resize(width, height uint, i image.Image) image.Image { 26 | return resize.Resize(width, height, i, resize.Lanczos3) 27 | } 28 | 29 | func (r defaultResizer) Thumbnail(maxWidth, maxHeight uint, i image.Image) image.Image { 30 | return resize.Thumbnail(maxWidth, maxHeight, i, resize.Lanczos3) 31 | } 32 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package imstor 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "log" 8 | 9 | "github.com/vincent-petithory/dataurl" 10 | ) 11 | 12 | func (s storage) StoreDataURL(str string) error { 13 | dataURL, err := dataurl.DecodeString(str) 14 | if err != nil { 15 | return err 16 | } 17 | return s.Store(dataURL.MediaType.ContentType(), dataURL.Data) 18 | } 19 | 20 | func (s storage) Store(mediaType string, data []byte) error { 21 | dataReader := bytes.NewReader(data) 22 | checksum := s.Checksum(data) 23 | for _, format := range s.conf.Formats { 24 | if mediaType == format.DecodableMediaType() { 25 | return s.storeInFormat(dataReader, checksum, format) 26 | } 27 | } 28 | return errors.New("Not a supported format!") 29 | } 30 | 31 | func (s storage) storeInFormat(r io.Reader, checksum string, f Format) error { 32 | image, err := f.Decode(r) 33 | if err != nil { 34 | return err 35 | } 36 | copies := createCopies(image, s.conf.CopySizes, s.resizer) 37 | folderPath := getAbsFolderPath(s.conf.RootPath, checksum) 38 | if err = createFolder(folderPath); err != nil { 39 | return err 40 | } 41 | if err = writeImageAndCopies(folderPath, image, copies, f); err != nil { 42 | log.Println("Writing an image failed, but a new folder and some files may have already been created. Please check your filesystem for clutter.") 43 | return err 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /writing.go: -------------------------------------------------------------------------------- 1 | package imstor 2 | 3 | import ( 4 | "fmt" 5 | "image" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | // rw-r----- 11 | const permission = 0750 12 | 13 | type imageFile struct { 14 | name string 15 | image image.Image 16 | } 17 | 18 | func createCopies(image image.Image, sizes []Size, resizer Resizer) []imageFile { 19 | copies := make([]imageFile, len(sizes)) 20 | for i, size := range sizes { 21 | imageCopy := resizer.Thumbnail(size.Width, size.Height, image) 22 | copies[i] = imageFile{ 23 | name: size.Name, 24 | image: imageCopy, 25 | } 26 | } 27 | return copies 28 | } 29 | 30 | func writeImageAndCopies(folder string, original image.Image, copies []imageFile, f Format) error { 31 | imageFiles := append(copies, imageFile{ 32 | name: originalImageName, 33 | image: original, 34 | }) 35 | return writeImageFiles(folder, imageFiles, f) 36 | } 37 | 38 | func writeImageFiles(folder string, imageFiles []imageFile, f Format) error { 39 | for _, imageFile := range imageFiles { 40 | fileName := fmt.Sprintf("%s.%s", imageFile.name, f.EncodedExtension()) 41 | path := filepath.Join(folder, fileName) 42 | file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, permission) 43 | if err != nil && !os.IsExist(err) { 44 | return err 45 | } 46 | if err = f.Encode(file, imageFile.image); err != nil { 47 | return err 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func getAbsFolderPath(rootPath string, checksum string) string { 54 | structuredFolderPath := filepath.FromSlash(getStructuredFolderPath(checksum)) 55 | return filepath.Join(rootPath, structuredFolderPath) 56 | } 57 | 58 | func createFolder(path string) error { 59 | return os.MkdirAll(path, permission) 60 | } 61 | --------------------------------------------------------------------------------