├── .gitignore ├── .idea ├── .gitignore ├── gostore.iml ├── modules.xml └── vcs.xml ├── README.md ├── assets └── images │ └── logo.png ├── bin └── .gitkeep ├── build.sh ├── cmd └── main │ └── main.go ├── go.mod ├── go.sum ├── internal ├── application │ ├── create_bucket_service.go │ ├── create_bucket_service_test.go │ ├── create_object_service.go │ ├── delete_bucket_service.go │ ├── delete_object_service.go │ ├── delete_object_service_test.go │ ├── generate_bucket_path_service.go │ ├── generate_bucket_path_service_test.go │ ├── generate_object_path_service.go │ ├── generate_object_path_service_test.go │ ├── get_buckets_in_bucket_service.go │ ├── get_buckets_in_bucket_service_test.go │ ├── get_buckets_service.go │ ├── get_buckets_service_test.go │ ├── get_objects_in_bucket_service.go │ └── get_objects_in_bucket_service_test.go ├── domain │ ├── entities │ │ ├── bucket.go │ │ ├── bucket_test.go │ │ ├── object.go │ │ └── object_test.go │ └── repositories │ │ ├── bucket_respository.go │ │ └── object_repository.go ├── infrastructure │ ├── controllers │ │ ├── create_bucket.go │ │ ├── create_object.go │ │ ├── delete_bucket.go │ │ ├── delete_object.go │ │ ├── download_object.go │ │ ├── dtos │ │ │ └── message.go │ │ ├── get_buckets.go │ │ ├── get_buckets_in_bucket.go │ │ └── get_objects_in_bucket.go │ └── repositories │ │ ├── file_bucket_repository.go │ │ ├── file_bucket_repository_test.go │ │ ├── file_object_repository.go │ │ ├── file_object_repository_test.go │ │ ├── ram_bucket_repository.go │ │ └── ram_object_repository.go └── shared │ ├── filesystem.go │ ├── filesystem_test.go │ ├── url_generator.go │ └── url_generator_test.go ├── migrations ├── 001_buckets.sql └── 002_objects.sql └── storage └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | storage/* 3 | README.old.md 4 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/gostore.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Gostore Logo
3 |

4 | 5 |

Simplistic and minimalist storage.

6 | 7 | ## Get started 8 | 9 | Download the latest version of gostore for your operating system. 10 | Make a directory at the same level of the binary. 11 | 12 | ```bash 13 | $ mkdir storage 14 | $ ls 15 | gostore_1.0.0_windows_amd64.exe* storage/ 16 | ``` 17 | 18 | Open a terminal an execute the application. 19 | Then, you can use the features' endpoints to interact with the storage api. 20 | 21 | ```bash 22 | $ curl --location 'localhost:80/api/v1/buckets' \ 23 | --header 'Content-Type: application/json' \ 24 | --data '{ 25 | "name": "my bucket" 26 | }' 27 | % Total % Received % Xferd Average Speed Time Time Time Current 28 | Dload Upload Total Spent Left Speed 29 | 100 109 100 82 100 27 821 270 --:--:-- --:--:-- --:--:-- 1101{"id":"6893ee7a-d370-4120-8735-9be2329c788a","name":"my bucket","parent_id":null} 30 | 31 | $ ls storage/ 32 | 6893ee7a-d370-4120-8735-9be2329c788a/ buckets.json 33 | ``` 34 | 35 | ## Flags 36 | 37 | You can customize the execution using the following flags: 38 | 39 | ``` 40 | -f string 41 | storage folder name (alias) (default "storage") 42 | -folder string 43 | storage folder name (default "storage") 44 | -h string 45 | host (alias) (default "localhost") 46 | -help 47 | shows help 48 | -host string 49 | host (default "localhost") 50 | -p int 51 | port (alias) (default 80) 52 | -port int 53 | port (default 80) 54 | ``` 55 | 56 | ## Features 57 | 58 | | Feature | Method | Endpoint (/api/v1) | 59 | |-----------------------------------------|--------|------------------------------| 60 | | List Buckets | GET | /buckets | 61 | | Create a Bucket | POST | /buckets | 62 | | Upload an Object to a Bucket | POST | /buckets/{bucketID}/objects | 63 | | List Buckets in a first level of Bucket | GET | /buckets/{bucketID}/buckets | 64 | | List Objects in a first level of Bucket | GET | /buckets/{bucketID}/objects | 65 | | Download an Object | GET | /objects/{objectID}/download | 66 | | Delete an Object | DELETE | /objects/{objectID} | 67 | | Delete a Bucket | DELETE | /buckets/{bucketID} | 68 | 69 | ## Repositories Implementations 70 | 71 | Store the information about buckets and objects as a metadata to process some requests simple as possible. 72 | 73 | | Implementation | Description | 74 | |----------------|----------------------------| 75 | | In Memory | Uses server's RAM | 76 | | File | Uses a JSON representation | 77 | 78 | ## How it works? 79 | 80 | **Gostore** save information into a local storage folder, keeping the structure with parents, but using uuids as a names. 81 | Buckets are stored as folders, objects are stored as files. 82 | 83 | When a request has started, the program give the control to the router, that send the request to some controller. 84 | Then, a service handle the action, interact with the filesystem (if necessary), and then save or update the metadata about the buckets or objects. 85 | This metadata helps the program to get information fast. 86 | 87 | **Gostore** was made following hexagonal architecture with some principles of DDD. 88 | 89 | ```mermaid 90 | sequenceDiagram 91 | main-->>+CreateBucketCtrl: Handle(request) 92 | CreateBucketCtrl-->>+CreateBucketServ: Do(name) 93 | CreateBucketServ-->>+Filesystem: MakeDirectory(path) 94 | Filesystem-->>-CreateBucketServ: error 95 | CreateBucketServ-->>+Bucket: new(id, name, parentID) 96 | Bucket-->>-CreateBucketServ: pointer 97 | CreateBucketServ-->>+BucketRepository: Save(bucket) 98 | BucketRepository-->>-CreateBucketServ: error 99 | CreateBucketServ-->>-CreateBucketCtrl: bucket, error 100 | CreateBucketCtrl-->-main: response 101 | ``` 102 | 103 | ## Documentation 104 | 105 | You can see much explained docs at the [Wiki](https://github.com/Jibaru/gostore/wiki). 106 | -------------------------------------------------------------------------------- /assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jibaru/gostore/976f222f69312ce48c86942bd3c726e77c49c81d/assets/images/logo.png -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jibaru/gostore/976f222f69312ce48c86942bd3c726e77c49c81d/bin/.gitkeep -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APP_NAME="gostore" 4 | VERSION="1.0.0" 5 | OUTPUT_DIR="bin" 6 | MAIN_FILE="./cmd/main/main.go" 7 | 8 | PLATFORMS=("windows/386" "windows/amd64" "linux/386" "linux/amd64" "darwin/amd64" "linux/arm") 9 | 10 | # Clean and create the output directory 11 | rm -rf "$OUTPUT_DIR" 12 | mkdir -p "$OUTPUT_DIR" 13 | 14 | # Iterate through platforms and compile the application 15 | for PLATFORM in "${PLATFORMS[@]}"; do 16 | GOOS=${PLATFORM%/*} 17 | GOARCH=${PLATFORM#*/} 18 | OUTPUT_NAME="$OUTPUT_DIR/${APP_NAME}_${VERSION}_${GOOS}_${GOARCH}" 19 | 20 | # Compile for the current platform 21 | if [[ "$GOOS" == "windows" ]]; then 22 | env GOOS=$GOOS GOARCH=$GOARCH go build -o "$OUTPUT_NAME".exe "$MAIN_FILE" 23 | else 24 | env GOOS=$GOOS GOARCH=$GOARCH go build -o "$OUTPUT_NAME" "$MAIN_FILE" 25 | fi 26 | 27 | # Check if the compilation was successful 28 | if [ $? -eq 0 ]; then 29 | echo "Compilation successful for $PLATFORM" 30 | else 31 | echo "Error compiling for $PLATFORM" 32 | fi 33 | done 34 | 35 | echo "Compilation complete. Binaries are available in the $OUTPUT_DIR directory." 36 | -------------------------------------------------------------------------------- /cmd/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "github.com/jibaru/gostore/internal/application" 6 | "github.com/jibaru/gostore/internal/infrastructure/controllers" 7 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 8 | "github.com/jibaru/gostore/internal/shared" 9 | "github.com/labstack/echo/v4" 10 | "github.com/labstack/echo/v4/middleware" 11 | "os" 12 | "strconv" 13 | ) 14 | 15 | func main() { 16 | var port int 17 | var host string 18 | var storageFolderName string 19 | var showsHelp bool 20 | 21 | flag.StringVar(&host, "h", "localhost", "host (alias)") 22 | flag.StringVar(&host, "host", "localhost", "host") 23 | 24 | flag.IntVar(&port, "p", 80, "port (alias)") 25 | flag.IntVar(&port, "port", 80, "port") 26 | 27 | flag.StringVar(&storageFolderName, "f", "storage", "storage folder name (alias)") 28 | flag.StringVar(&storageFolderName, "folder", "storage", "storage folder name") 29 | 30 | flag.BoolVar(&showsHelp, "help", false, "shows help") 31 | 32 | flag.Parse() 33 | 34 | if showsHelp { 35 | flag.PrintDefaults() 36 | os.Exit(0) 37 | } 38 | 39 | address := host + ":" + strconv.Itoa(port) 40 | 41 | urlGenerator := shared.NewUrlGenerator( 42 | "localhost", 43 | uint(port), 44 | storageFolderName, 45 | false, 46 | ) 47 | filesystem := shared.NewServerFilesystem("./" + storageFolderName) 48 | 49 | bucketRepository, err := repositories.NewFileBucketRepository(storageFolderName + "/buckets.json") 50 | if err != nil { 51 | panic(err) 52 | return 53 | } 54 | objectRepository, err := repositories.NewFileObjectRepository(storageFolderName + "/objects.json") 55 | if err != nil { 56 | panic(err) 57 | return 58 | } 59 | 60 | getBucketsService := application.NewGetBucketsService( 61 | bucketRepository, 62 | ) 63 | generateBucketPathServ := application.NewGenerateBucketPathService( 64 | bucketRepository, 65 | ) 66 | generateObjectPathServ := application.NewGenerateObjectPathService( 67 | objectRepository, 68 | generateBucketPathServ, 69 | ) 70 | createBucketServ := application.NewCreateBucketService( 71 | bucketRepository, 72 | filesystem, 73 | generateBucketPathServ, 74 | ) 75 | createObjectServ := application.NewCreateObjectService( 76 | bucketRepository, 77 | objectRepository, 78 | filesystem, 79 | generateBucketPathServ, 80 | ) 81 | getBucketsInBucketServ := application.NewGetBucketsInBucketService( 82 | bucketRepository, 83 | ) 84 | getObjectsInBucketServ := application.NewGetObjectsInBucketService( 85 | objectRepository, 86 | ) 87 | deleteObjectServ := application.NewDeleteObjectService( 88 | objectRepository, 89 | generateObjectPathServ, 90 | filesystem, 91 | ) 92 | deleteBucketServ := application.NewDeleteBucketService( 93 | bucketRepository, 94 | generateBucketPathServ, 95 | filesystem, 96 | ) 97 | 98 | e := echo.New() 99 | e.Use(middleware.Logger()) 100 | 101 | e.Static("/storage", storageFolderName) 102 | 103 | api := e.Group("/api/v1") 104 | api.POST("/buckets", controllers.NewCreateBucket(createBucketServ).Handle) 105 | api.POST("/buckets/:bucketID/objects", controllers.NewCreateObject(createObjectServ).Handle) 106 | api.GET("/buckets/:bucketID/objects", controllers.NewGetObjectsInBucket(getObjectsInBucketServ).Handle) 107 | api.GET("/buckets", controllers.NewGetBuckets(getBucketsService).Handle) 108 | api.GET("/buckets/:bucketID/buckets", controllers.NewGetBucketsInBucket(getBucketsInBucketServ).Handle) 109 | api.GET("/objects/:objectID/download", controllers.NewDownloadObject(urlGenerator, generateObjectPathServ).Handle) 110 | api.DELETE("/objects/:objectID", controllers.NewDeleteObject(deleteObjectServ).Handle) 111 | api.DELETE("/buckets/:bucketID", controllers.NewDeleteBucket(deleteBucketServ).Handle) 112 | 113 | e.Logger.Fatal(e.Start(address)) 114 | } 115 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jibaru/gostore 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/google/uuid v1.3.1 7 | github.com/labstack/echo/v4 v4.11.1 8 | github.com/stretchr/testify v1.8.4 9 | ) 10 | 11 | require ( 12 | github.com/davecgh/go-spew v1.1.1 // indirect 13 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect 14 | github.com/kr/pretty v0.3.0 // indirect 15 | github.com/labstack/gommon v0.4.0 // indirect 16 | github.com/mattn/go-colorable v0.1.13 // indirect 17 | github.com/mattn/go-isatty v0.0.19 // indirect 18 | github.com/pmezard/go-difflib v1.0.0 // indirect 19 | github.com/valyala/bytebufferpool v1.0.0 // indirect 20 | github.com/valyala/fasttemplate v1.2.2 // indirect 21 | golang.org/x/crypto v0.11.0 // indirect 22 | golang.org/x/net v0.12.0 // indirect 23 | golang.org/x/sys v0.10.0 // indirect 24 | golang.org/x/text v0.11.0 // indirect 25 | golang.org/x/time v0.3.0 // indirect 26 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= 6 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 7 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 8 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 9 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 10 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 11 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 12 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 15 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 16 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 | github.com/labstack/echo/v4 v4.11.1 h1:dEpLU2FLg4UVmvCGPuk/APjlH6GDpbEPti61srUUUs4= 18 | github.com/labstack/echo/v4 v4.11.1/go.mod h1:YuYRTSM3CHs2ybfrL8Px48bO6BAnYIN4l8wSTMP6BDQ= 19 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= 20 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= 21 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 22 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 23 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 24 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 25 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 26 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 27 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 28 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 29 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 30 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 31 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 34 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 35 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 36 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 37 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 38 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 39 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 40 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 41 | golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= 42 | golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= 43 | golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= 44 | golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= 45 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 46 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 47 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 48 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= 51 | golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= 53 | golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 54 | golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 55 | golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 56 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 57 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 58 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 59 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 60 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 61 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 62 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 63 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 64 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 65 | -------------------------------------------------------------------------------- /internal/application/create_bucket_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/domain/repositories" 7 | "github.com/jibaru/gostore/internal/shared" 8 | ) 9 | 10 | type CreateBucketService struct { 11 | bucketRepository repositories.BucketRepository 12 | filesystem shared.Filesystem 13 | generateBucketPathService GenerateBucketPathServiceInputPort 14 | } 15 | 16 | func NewCreateBucketService( 17 | bucketRepository repositories.BucketRepository, 18 | filesystem shared.Filesystem, 19 | generateBucketPathService GenerateBucketPathServiceInputPort, 20 | ) *CreateBucketService { 21 | return &CreateBucketService{ 22 | bucketRepository, 23 | filesystem, 24 | generateBucketPathService, 25 | } 26 | } 27 | 28 | func (serv *CreateBucketService) Do( 29 | name string, 30 | parentID *string, 31 | ) (*entities.Bucket, error) { 32 | bucketID := uuid.New().String() 33 | 34 | if parentID != nil { 35 | parentPath, err := serv.generateBucketPathService.Do(*parentID) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | err = serv.filesystem.MakeDirectoryOnPath(bucketID, parentPath) 41 | if err != nil { 42 | return nil, err 43 | } 44 | } else { 45 | err := serv.filesystem.MakeDirectory(bucketID) 46 | if err != nil { 47 | return nil, err 48 | } 49 | } 50 | 51 | bucket, err := entities.NewBucket(bucketID, name, parentID) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | err = serv.bucketRepository.Save(*bucket) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | return bucket, nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/application/create_bucket_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 7 | "github.com/jibaru/gostore/internal/shared" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestCreateBucketWithEmptyParent(t *testing.T) { 13 | ramBucketRepository := repositories.NewEmptyRamBucketRepository() 14 | dummyFilesystem := shared.NewDummyFilesystem() 15 | bucketPathService := NewCallableGenerateBucketPathServiceForRootBucket() 16 | createBucketService := NewCreateBucketService(ramBucketRepository, dummyFilesystem, bucketPathService) 17 | 18 | bucket, err := createBucketService.Do("test_bucket", nil) 19 | 20 | assert.Nil(t, err) 21 | assert.NotNil(t, bucket) 22 | } 23 | 24 | func TestCreateBucketWithEmptyNameFails(t *testing.T) { 25 | ramBucketRepository := repositories.NewEmptyRamBucketRepository() 26 | dummyFilesystem := shared.NewDummyFilesystem() 27 | bucketPathService := NewCallableGenerateBucketPathServiceForRootBucket() 28 | createBucketService := NewCreateBucketService(ramBucketRepository, dummyFilesystem, bucketPathService) 29 | 30 | bucket, err := createBucketService.Do("", nil) 31 | 32 | assert.NotNil(t, err) 33 | assert.Nil(t, bucket) 34 | } 35 | 36 | func TestCreateBucketWithNotExistsParentFails(t *testing.T) { 37 | ramBucketRepository := repositories.NewEmptyRamBucketRepository() 38 | dummyFilesystem := shared.NewDummyFilesystem() 39 | bucketPathService := NewCallableGenerateBucketPathServiceForRootBucket() 40 | createBucketService := NewCreateBucketService(ramBucketRepository, dummyFilesystem, bucketPathService) 41 | parentID := uuid.New().String() 42 | 43 | bucket, err := createBucketService.Do("test_bucket", &parentID) 44 | 45 | assert.Nil(t, err) 46 | assert.NotNil(t, bucket) 47 | } 48 | 49 | func TestCreateBucketWithExistsParent(t *testing.T) { 50 | parentID := uuid.New().String() 51 | buckets := make([]entities.Bucket, 0) 52 | buckets = append(buckets, entities.Bucket{ 53 | ID: parentID, 54 | Name: "test_bucket", 55 | ParentID: nil, 56 | }) 57 | ramBucketRepository := repositories.NewRamBucketRepository(buckets) 58 | dummyFilesystem := shared.NewDummyFilesystem() 59 | bucketPathService := NewCallableGenerateBucketPathServiceForRootBucket() 60 | createBucketService := NewCreateBucketService(ramBucketRepository, dummyFilesystem, bucketPathService) 61 | 62 | bucket, err := createBucketService.Do("test_bucket", &parentID) 63 | 64 | assert.Nil(t, err) 65 | assert.NotNil(t, bucket) 66 | } 67 | -------------------------------------------------------------------------------- /internal/application/create_object_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/domain/repositories" 7 | "github.com/jibaru/gostore/internal/shared" 8 | "io" 9 | "mime/multipart" 10 | "path/filepath" 11 | ) 12 | 13 | type CreateObjectService struct { 14 | bucketRepository repositories.BucketRepository 15 | objectRepository repositories.ObjectRepository 16 | filesystem shared.Filesystem 17 | generateBucketPathService GenerateBucketPathServiceInputPort 18 | } 19 | 20 | func NewCreateObjectService( 21 | bucketRepository repositories.BucketRepository, 22 | objectRepository repositories.ObjectRepository, 23 | filesystem shared.Filesystem, 24 | generateBucketPathService GenerateBucketPathServiceInputPort, 25 | ) *CreateObjectService { 26 | return &CreateObjectService{ 27 | bucketRepository, 28 | objectRepository, 29 | filesystem, 30 | generateBucketPathService, 31 | } 32 | } 33 | 34 | func (serv *CreateObjectService) Do( 35 | file *multipart.FileHeader, 36 | bucketID string, 37 | ) (*entities.Object, error) { 38 | objectID := uuid.New().String() 39 | bucketPath, err := serv.generateBucketPathService.Do(bucketID) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | src, err := file.Open() 45 | defer src.Close() 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | extension := filepath.Ext(file.Filename) 51 | 52 | dst, err := serv.filesystem.MakeFileOnPath(objectID+extension, bucketPath) 53 | defer dst.Close() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if _, err = io.Copy(dst, src); err != nil { 59 | return nil, err 60 | } 61 | 62 | object, err := entities.NewObject(objectID, file.Filename, extension, bucketID) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | err = serv.objectRepository.Save(*object) 68 | if err != nil { 69 | return nil, err 70 | } 71 | 72 | return object, nil 73 | } 74 | -------------------------------------------------------------------------------- /internal/application/delete_bucket_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/domain/repositories" 5 | "github.com/jibaru/gostore/internal/shared" 6 | ) 7 | 8 | type DeleteBucketServiceInputPort interface { 9 | Do(bucketID string) error 10 | } 11 | 12 | type DeleteBucketService struct { 13 | bucketRepository repositories.BucketRepository 14 | generateBucketPathService GenerateBucketPathServiceInputPort 15 | filesystem shared.Filesystem 16 | } 17 | 18 | func NewDeleteBucketService( 19 | bucketRepository repositories.BucketRepository, 20 | generateBucketPathService GenerateBucketPathServiceInputPort, 21 | filesystem shared.Filesystem, 22 | ) DeleteBucketServiceInputPort { 23 | return &DeleteBucketService{ 24 | bucketRepository, 25 | generateBucketPathService, 26 | filesystem, 27 | } 28 | } 29 | 30 | func (serv *DeleteBucketService) Do(bucketID string) error { 31 | path, err := serv.generateBucketPathService.Do(bucketID) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | err = serv.filesystem.DeleteDirectoryOnPath(path) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | err = serv.bucketRepository.DeleteByID(bucketID) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/application/delete_object_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/domain/repositories" 5 | "github.com/jibaru/gostore/internal/shared" 6 | ) 7 | 8 | type DeleteObjectServiceInputPort interface { 9 | Do(objectID string) error 10 | } 11 | 12 | type DeleteObjectService struct { 13 | objectRepository repositories.ObjectRepository 14 | generateObjectPathService GenerateObjectPathServiceInputPort 15 | filesystem shared.Filesystem 16 | } 17 | 18 | func NewDeleteObjectService( 19 | objectRepository repositories.ObjectRepository, 20 | generateObjectPathService GenerateObjectPathServiceInputPort, 21 | filesystem shared.Filesystem, 22 | ) DeleteObjectServiceInputPort { 23 | return &DeleteObjectService{ 24 | objectRepository, 25 | generateObjectPathService, 26 | filesystem, 27 | } 28 | } 29 | 30 | func (serv *DeleteObjectService) Do(objectID string) error { 31 | path, err := serv.generateObjectPathService.Do(objectID) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | err = serv.objectRepository.DeleteByID(objectID) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | err = serv.filesystem.DeleteFileOnPath(path) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/application/delete_object_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 7 | "github.com/jibaru/gostore/internal/shared" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestDeleteObjectService(t *testing.T) { 13 | objectID := uuid.NewString() 14 | bucketID := uuid.NewString() 15 | extension := ".png" 16 | generateObjectPathServ := NewCallableGenerateObjectPathServiceForValidObject( 17 | bucketID, 18 | extension, 19 | ) 20 | objectRepository := repositories.NewRamObjectRepository([]entities.Object{ 21 | {ID: objectID, Name: "object_1", Extension: extension, BucketID: bucketID}, 22 | {ID: uuid.NewString(), Name: "object_2", Extension: extension, BucketID: bucketID}, 23 | }) 24 | filesystem := shared.NewDummyFilesystem() 25 | service := NewDeleteObjectService( 26 | objectRepository, 27 | generateObjectPathServ, 28 | filesystem, 29 | ) 30 | 31 | err := service.Do(objectID) 32 | 33 | assert.Nil(t, err) 34 | assert.Equal(t, 1, objectRepository.Size()) 35 | } 36 | 37 | func TestDeleteObjectServiceFails(t *testing.T) { 38 | objectID := uuid.NewString() 39 | bucketID := uuid.NewString() 40 | extension := ".png" 41 | generateObjectPathServ := NewCallableGenerateObjectPathServiceForValidObject( 42 | bucketID, 43 | extension, 44 | ) 45 | objectRepository := repositories.NewRamObjectRepository([]entities.Object{}) 46 | filesystem := shared.NewDummyFilesystem() 47 | service := NewDeleteObjectService( 48 | objectRepository, 49 | generateObjectPathServ, 50 | filesystem, 51 | ) 52 | 53 | err := service.Do(objectID) 54 | 55 | assert.NotNil(t, err) 56 | assert.Equal(t, 0, objectRepository.Size()) 57 | } 58 | -------------------------------------------------------------------------------- /internal/application/generate_bucket_path_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import "github.com/jibaru/gostore/internal/domain/repositories" 4 | 5 | type GenerateBucketPathServiceInputPort interface { 6 | Do(bucketID string) (string, error) 7 | } 8 | 9 | type GenerateBucketPathService struct { 10 | bucketRepository repositories.BucketRepository 11 | } 12 | 13 | type CallableGenerateBucketPathService struct { 14 | onDo func(bucketID string) (string, error) 15 | } 16 | 17 | func NewGenerateBucketPathService( 18 | bucketRepository repositories.BucketRepository, 19 | ) GenerateBucketPathServiceInputPort { 20 | return &GenerateBucketPathService{ 21 | bucketRepository, 22 | } 23 | } 24 | 25 | func NewCallableGenerateBucketPathServiceForRootBucket() GenerateBucketPathServiceInputPort { 26 | return &CallableGenerateBucketPathService{ 27 | func(bucketID string) (string, error) { 28 | return "/" + bucketID, nil 29 | }, 30 | } 31 | } 32 | 33 | func (serv *GenerateBucketPathService) Do(bucketID string) (string, error) { 34 | bucket, err := serv.bucketRepository.FindByID(bucketID) 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | if bucket.InRoot() { 40 | return "/" + bucket.ID, nil 41 | } 42 | 43 | path, err := serv.Do(*bucket.ParentID) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | return path + "/" + bucketID, nil 49 | } 50 | 51 | func (s *CallableGenerateBucketPathService) Do(bucketID string) (string, error) { 52 | return s.onDo(bucketID) 53 | } 54 | -------------------------------------------------------------------------------- /internal/application/generate_bucket_path_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestGenerateBucketPathServiceWithBucketWithEmptyParent(t *testing.T) { 12 | bucketID := uuid.New().String() 13 | buckets := make([]entities.Bucket, 0) 14 | buckets = append(buckets, entities.Bucket{ 15 | ID: bucketID, 16 | Name: "bucket_test", 17 | ParentID: nil, 18 | }) 19 | bucketRepository := repositories.NewRamBucketRepository(buckets) 20 | service := NewGenerateBucketPathService( 21 | bucketRepository, 22 | ) 23 | expectedPath := "/" + bucketID 24 | 25 | path, err := service.Do(bucketID) 26 | 27 | assert.Nil(t, err) 28 | assert.Equal(t, expectedPath, path) 29 | } 30 | 31 | func TestGenerateBucketPathServiceWithBucketWithNestedParent(t *testing.T) { 32 | bucketID := uuid.New().String() 33 | parentID := uuid.New().String() 34 | rootID := uuid.New().String() 35 | buckets := make([]entities.Bucket, 0) 36 | buckets = append(buckets, entities.Bucket{ 37 | ID: rootID, 38 | Name: "bucket_test", 39 | ParentID: nil, 40 | }) 41 | buckets = append(buckets, entities.Bucket{ 42 | ID: parentID, 43 | Name: "parent_bucket_test", 44 | ParentID: &rootID, 45 | }) 46 | buckets = append(buckets, entities.Bucket{ 47 | ID: bucketID, 48 | Name: "bucket_test", 49 | ParentID: &parentID, 50 | }) 51 | bucketRepository := repositories.NewRamBucketRepository(buckets) 52 | service := NewGenerateBucketPathService( 53 | bucketRepository, 54 | ) 55 | expectedPath := "/" + rootID + "/" + parentID + "/" + bucketID 56 | 57 | path, err := service.Do(bucketID) 58 | 59 | assert.Nil(t, err) 60 | assert.Equal(t, expectedPath, path) 61 | } 62 | 63 | func TestGenerateBucketPathServiceWithInvalidBucketIDFails(t *testing.T) { 64 | bucketRepository := repositories.NewRamBucketRepository(make([]entities.Bucket, 0)) 65 | service := NewGenerateBucketPathService( 66 | bucketRepository, 67 | ) 68 | bucketID := uuid.New().String() 69 | 70 | path, err := service.Do(bucketID) 71 | 72 | assert.NotNil(t, err) 73 | assert.Empty(t, path) 74 | } 75 | -------------------------------------------------------------------------------- /internal/application/generate_object_path_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import "github.com/jibaru/gostore/internal/domain/repositories" 4 | 5 | type GenerateObjectPathServiceInputPort interface { 6 | Do(objectID string) (string, error) 7 | } 8 | 9 | type GenerateObjectPathService struct { 10 | objectRepository repositories.ObjectRepository 11 | generateBucketPathService GenerateBucketPathServiceInputPort 12 | } 13 | 14 | type CallableGenerateObjectPathService struct { 15 | onDo func(objectID string) (string, error) 16 | } 17 | 18 | func NewGenerateObjectPathService( 19 | objectRepository repositories.ObjectRepository, 20 | generateBucketPathService GenerateBucketPathServiceInputPort, 21 | ) GenerateObjectPathServiceInputPort { 22 | return &GenerateObjectPathService{ 23 | objectRepository, 24 | generateBucketPathService, 25 | } 26 | } 27 | 28 | func NewCallableGenerateObjectPathServiceForValidObject( 29 | bucketID string, 30 | extension string, 31 | ) GenerateBucketPathServiceInputPort { 32 | return &CallableGenerateBucketPathService{ 33 | func(objectID string) (string, error) { 34 | return "/" + bucketID + "/" + objectID + extension, nil 35 | }, 36 | } 37 | } 38 | 39 | func (serv *GenerateObjectPathService) Do(objectID string) (string, error) { 40 | object, err := serv.objectRepository.FindByID(objectID) 41 | if err != nil { 42 | return "", err 43 | } 44 | 45 | path, err := serv.generateBucketPathService.Do(object.BucketID) 46 | if err != nil { 47 | return "", err 48 | } 49 | 50 | return path + "/" + objectID + object.Extension, nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/application/generate_object_path_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | "github.com/jibaru/gostore/internal/domain/entities" 7 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func TestGenerateObjectPathService(t *testing.T) { 13 | objectID := uuid.New().String() 14 | bucketID := uuid.New().String() 15 | extension := ".png" 16 | objects := make([]entities.Object, 0) 17 | objects = append(objects, entities.Object{ 18 | ID: objectID, 19 | Name: "object_test", 20 | Extension: extension, 21 | BucketID: bucketID, 22 | }) 23 | objectRepository := repositories.NewRamObjectRepository(objects) 24 | bucketPathService := NewCallableGenerateBucketPathServiceForRootBucket() 25 | service := NewGenerateObjectPathService( 26 | objectRepository, 27 | bucketPathService, 28 | ) 29 | expectedPath := "/" + bucketID + "/" + objectID + extension 30 | 31 | path, err := service.Do(objectID) 32 | 33 | assert.Nil(t, err) 34 | assert.Equal(t, expectedPath, path) 35 | } 36 | 37 | func TestGenerateObjectPathServiceWithInvalidBucketFails(t *testing.T) { 38 | objectID := uuid.New().String() 39 | bucketID := uuid.New().String() 40 | extension := ".png" 41 | objects := make([]entities.Object, 0) 42 | objects = append(objects, entities.Object{ 43 | ID: objectID, 44 | Name: "object_test", 45 | Extension: extension, 46 | BucketID: bucketID, 47 | }) 48 | objectRepository := repositories.NewRamObjectRepository(objects) 49 | bucketPathService := CallableGenerateBucketPathService{ 50 | onDo: func(bucketID string) (string, error) { 51 | return "", errors.New("an error has occurred") 52 | }, 53 | } 54 | service := NewGenerateObjectPathService( 55 | objectRepository, 56 | &bucketPathService, 57 | ) 58 | 59 | path, err := service.Do(objectID) 60 | 61 | assert.NotNil(t, err) 62 | assert.Empty(t, path) 63 | } 64 | 65 | func TestGenerateObjectPathServiceWithInvalidObjectFails(t *testing.T) { 66 | objectID := uuid.New().String() 67 | objects := make([]entities.Object, 0) 68 | objectRepository := repositories.NewRamObjectRepository(objects) 69 | bucketPathService := CallableGenerateBucketPathService{ 70 | onDo: func(bucketID string) (string, error) { 71 | return "", nil 72 | }, 73 | } 74 | service := NewGenerateObjectPathService( 75 | objectRepository, 76 | &bucketPathService, 77 | ) 78 | 79 | path, err := service.Do(objectID) 80 | 81 | assert.NotNil(t, err) 82 | assert.Empty(t, path) 83 | } 84 | -------------------------------------------------------------------------------- /internal/application/get_buckets_in_bucket_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "errors" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/domain/repositories" 7 | ) 8 | 9 | type GetBucketsInBucketServiceInputPort interface { 10 | Do(bucketID string) ([]entities.Bucket, error) 11 | } 12 | 13 | type GetBucketsInBucketService struct { 14 | bucketsRepository repositories.BucketRepository 15 | } 16 | 17 | func NewGetBucketsInBucketService( 18 | bucketsRepository repositories.BucketRepository, 19 | ) GetBucketsInBucketServiceInputPort { 20 | return &GetBucketsInBucketService{ 21 | bucketsRepository, 22 | } 23 | } 24 | 25 | func (serv *GetBucketsInBucketService) Do(bucketID string) ([]entities.Bucket, error) { 26 | buckets, err := serv.bucketsRepository.GetByParentID(bucketID) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if len(buckets) == 0 { 32 | return nil, errors.New("buckets not found") 33 | } 34 | 35 | return buckets, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/application/get_buckets_in_bucket_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestGetBucketsInBucketService(t *testing.T) { 12 | parentID := uuid.New().String() 13 | anotherParentID := uuid.New().String() 14 | buckets := []entities.Bucket{ 15 | {ID: uuid.New().String(), Name: "test_bucket_1", ParentID: &parentID}, 16 | {ID: uuid.New().String(), Name: "test_bucket_2", ParentID: nil}, 17 | {ID: uuid.New().String(), Name: "test_bucket_3", ParentID: &parentID}, 18 | {ID: uuid.New().String(), Name: "test_bucket_4", ParentID: &anotherParentID}, 19 | } 20 | 21 | bucketRepository := repositories.NewRamBucketRepository(buckets) 22 | expectedBuckets := []entities.Bucket{ 23 | buckets[0], 24 | buckets[2], 25 | } 26 | service := NewGetBucketsInBucketService(bucketRepository) 27 | 28 | buckets, err := service.Do(parentID) 29 | 30 | assert.Nil(t, err) 31 | assert.NotNil(t, buckets) 32 | assert.NotEmpty(t, buckets) 33 | assert.Len(t, buckets, 2) 34 | assert.Equal(t, expectedBuckets, buckets) 35 | } 36 | 37 | func TestGetBucketsInBucketServiceFails(t *testing.T) { 38 | parentID := uuid.New().String() 39 | anotherParentID := uuid.New().String() 40 | 41 | bucketRepository := repositories.NewRamBucketRepository([]entities.Bucket{ 42 | {ID: uuid.New().String(), Name: "test_bucket_1", ParentID: &parentID}, 43 | {ID: uuid.New().String(), Name: "test_bucket_2", ParentID: nil}, 44 | {ID: uuid.New().String(), Name: "test_bucket_3", ParentID: &parentID}, 45 | }) 46 | service := NewGetBucketsInBucketService(bucketRepository) 47 | 48 | buckets, err := service.Do(anotherParentID) 49 | 50 | assert.NotNil(t, err) 51 | assert.Nil(t, buckets) 52 | } 53 | -------------------------------------------------------------------------------- /internal/application/get_buckets_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "errors" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/domain/repositories" 7 | ) 8 | 9 | type GetBucketServiceInputPort interface { 10 | Do() ([]entities.Bucket, error) 11 | } 12 | 13 | type GetBucketsService struct { 14 | bucketRepository repositories.BucketRepository 15 | } 16 | 17 | func NewGetBucketsService( 18 | bucketRepository repositories.BucketRepository, 19 | ) GetBucketServiceInputPort { 20 | return &GetBucketsService{ 21 | bucketRepository, 22 | } 23 | } 24 | 25 | func (serv *GetBucketsService) Do() ([]entities.Bucket, error) { 26 | buckets, err := serv.bucketRepository.GetAll() 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if len(buckets) == 0 { 32 | return nil, errors.New("buckets not found") 33 | } 34 | 35 | return buckets, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/application/get_buckets_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestGetBucketsService(t *testing.T) { 12 | firstBucketID := uuid.NewString() 13 | expectedBuckets := []entities.Bucket{ 14 | {ID: firstBucketID, Name: "test_bucket_1", ParentID: nil}, 15 | {ID: uuid.NewString(), Name: "test_bucket_2", ParentID: &firstBucketID}, 16 | } 17 | bucketRepository := repositories.NewRamBucketRepository(expectedBuckets) 18 | service := NewGetBucketsService(bucketRepository) 19 | 20 | buckets, err := service.Do() 21 | 22 | assert.Nil(t, err) 23 | assert.NotNil(t, buckets) 24 | assert.Equal(t, expectedBuckets, buckets) 25 | } 26 | 27 | func TestGetBucketsServiceFails(t *testing.T) { 28 | bucketRepository := repositories.NewEmptyRamBucketRepository() 29 | service := NewGetBucketsService(bucketRepository) 30 | 31 | buckets, err := service.Do() 32 | 33 | assert.NotNil(t, err) 34 | assert.Nil(t, buckets) 35 | } 36 | -------------------------------------------------------------------------------- /internal/application/get_objects_in_bucket_service.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "errors" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/domain/repositories" 7 | ) 8 | 9 | type GetObjectsInBucketServiceInputPort interface { 10 | Do(bucketID string) ([]entities.Object, error) 11 | } 12 | 13 | type GetObjectsInBucketService struct { 14 | objectRepository repositories.ObjectRepository 15 | } 16 | 17 | func NewGetObjectsInBucketService( 18 | objectRepository repositories.ObjectRepository, 19 | ) GetObjectsInBucketServiceInputPort { 20 | return &GetObjectsInBucketService{ 21 | objectRepository, 22 | } 23 | } 24 | 25 | func (serv *GetObjectsInBucketService) Do(bucketID string) ([]entities.Object, error) { 26 | objects, err := serv.objectRepository.GetByBucketID(bucketID) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | if len(objects) == 0 { 32 | return nil, errors.New("objects not found") 33 | } 34 | 35 | return objects, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/application/get_objects_in_bucket_service_test.go: -------------------------------------------------------------------------------- 1 | package application 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/infrastructure/repositories" 7 | "github.com/stretchr/testify/assert" 8 | "testing" 9 | ) 10 | 11 | func TestGetObjectsInBucketService(t *testing.T) { 12 | bucketID := uuid.New().String() 13 | objects := []entities.Object{ 14 | {ID: uuid.New().String(), Name: "test_object_1", Extension: ".png", BucketID: bucketID}, 15 | {ID: uuid.New().String(), Name: "test_object_2", Extension: ".png", BucketID: uuid.New().String()}, 16 | {ID: uuid.New().String(), Name: "test_object_3", Extension: ".png", BucketID: bucketID}, 17 | {ID: uuid.New().String(), Name: "test_object_4", Extension: ".png", BucketID: uuid.New().String()}, 18 | } 19 | 20 | objectRepository := repositories.NewRamObjectRepository(objects) 21 | expectedObjects := []entities.Object{ 22 | objects[0], 23 | objects[2], 24 | } 25 | service := NewGetObjectsInBucketService(objectRepository) 26 | 27 | objects, err := service.Do(bucketID) 28 | 29 | assert.Nil(t, err) 30 | assert.NotNil(t, objects) 31 | assert.NotEmpty(t, objects) 32 | assert.Len(t, objects, 2) 33 | assert.Equal(t, expectedObjects, objects) 34 | } 35 | 36 | func TestGetObjectsInBucketServiceFails(t *testing.T) { 37 | bucketID := uuid.New().String() 38 | objects := []entities.Object{ 39 | {ID: uuid.New().String(), Name: "test_object_1", Extension: ".png", BucketID: uuid.New().String()}, 40 | {ID: uuid.New().String(), Name: "test_object_2", Extension: ".png", BucketID: uuid.New().String()}, 41 | } 42 | 43 | objectRepository := repositories.NewRamObjectRepository(objects) 44 | service := NewGetObjectsInBucketService(objectRepository) 45 | 46 | objects, err := service.Do(bucketID) 47 | 48 | assert.NotNil(t, err) 49 | assert.Nil(t, objects) 50 | } 51 | -------------------------------------------------------------------------------- /internal/domain/entities/bucket.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | "strings" 7 | ) 8 | 9 | type Bucket struct { 10 | ID string `json:"id" gorm:"primaryKey;type:uuid"` 11 | Name string `json:"name"` 12 | ParentID *string `json:"parent_id" gorm:"type:uuid"` 13 | } 14 | 15 | func NewBucket( 16 | id string, 17 | name string, 18 | parentID *string, 19 | ) (*Bucket, error) { 20 | if len(strings.TrimSpace(id)) == 0 { 21 | return nil, errors.New("id should not be empty") 22 | } 23 | 24 | if _, err := uuid.Parse(id); err != nil { 25 | return nil, errors.New("id should be a uuid") 26 | } 27 | 28 | if len(strings.TrimSpace(name)) == 0 { 29 | return nil, errors.New("name should not be empty") 30 | } 31 | 32 | if parentID != nil { 33 | if len(strings.TrimSpace(*parentID)) == 0 { 34 | return nil, errors.New("parent id should not be empty") 35 | } 36 | 37 | if _, err := uuid.Parse(*parentID); err != nil { 38 | return nil, errors.New("parent id should be a uuid") 39 | } 40 | } 41 | 42 | return &Bucket{ 43 | ID: id, 44 | Name: name, 45 | ParentID: parentID, 46 | }, nil 47 | } 48 | 49 | func (that *Bucket) InRoot() bool { 50 | return that.ParentID == nil 51 | } 52 | -------------------------------------------------------------------------------- /internal/domain/entities/bucket_test.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewBucketWithEmptyParent(t *testing.T) { 10 | bucketID := uuid.New().String() 11 | name := "test_bucket" 12 | 13 | bucket, err := NewBucket( 14 | bucketID, 15 | name, 16 | nil, 17 | ) 18 | 19 | assert.Nil(t, err) 20 | assert.Equal(t, bucketID, bucket.ID) 21 | assert.Equal(t, name, bucket.Name) 22 | assert.Nil(t, bucket.ParentID) 23 | } 24 | 25 | func TestNewBucketWithParent(t *testing.T) { 26 | bucketID := uuid.New().String() 27 | name := "test_bucket" 28 | parentID := uuid.New().String() 29 | 30 | bucket, err := NewBucket( 31 | bucketID, 32 | name, 33 | &parentID, 34 | ) 35 | 36 | assert.Nil(t, err) 37 | assert.Equal(t, bucketID, bucket.ID) 38 | assert.Equal(t, name, bucket.Name) 39 | assert.Equal(t, parentID, *bucket.ParentID) 40 | } 41 | 42 | func TestNewBucketWithEmptyIDFails(t *testing.T) { 43 | bucketID := "" 44 | name := "test_bucket" 45 | 46 | bucket, err := NewBucket( 47 | bucketID, 48 | name, 49 | nil, 50 | ) 51 | 52 | assert.NotNil(t, err) 53 | assert.Nil(t, bucket) 54 | } 55 | 56 | func TestNewBucketWithEmptyNameFails(t *testing.T) { 57 | bucketID := uuid.New().String() 58 | name := "" 59 | 60 | bucket, err := NewBucket( 61 | bucketID, 62 | name, 63 | nil, 64 | ) 65 | 66 | assert.NotNil(t, err) 67 | assert.Nil(t, bucket) 68 | } 69 | 70 | func TestNewBucketWithEmptyParentIDFails(t *testing.T) { 71 | bucketID := uuid.New().String() 72 | name := "test_bucket" 73 | parentID := "" 74 | 75 | bucket, err := NewBucket( 76 | bucketID, 77 | name, 78 | &parentID, 79 | ) 80 | 81 | assert.NotNil(t, err) 82 | assert.Nil(t, bucket) 83 | } 84 | 85 | func TestNewBucketWithInvalidIDFails(t *testing.T) { 86 | bucketID := "random_id" 87 | name := "test_bucket" 88 | 89 | bucket, err := NewBucket( 90 | bucketID, 91 | name, 92 | nil, 93 | ) 94 | 95 | assert.NotNil(t, err) 96 | assert.Nil(t, bucket) 97 | } 98 | 99 | func TestNewBucketWithInvalidParentIDFails(t *testing.T) { 100 | bucketID := uuid.New().String() 101 | name := "test_bucket" 102 | parentID := "random_id" 103 | 104 | bucket, err := NewBucket( 105 | bucketID, 106 | name, 107 | &parentID, 108 | ) 109 | 110 | assert.NotNil(t, err) 111 | assert.Nil(t, bucket) 112 | } 113 | -------------------------------------------------------------------------------- /internal/domain/entities/object.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | "strings" 7 | ) 8 | 9 | type Object struct { 10 | ID string `gorm:"primaryKey;type:uuid" json:"id"` 11 | Name string `json:"name"` 12 | Extension string `json:"extension"` 13 | BucketID string `gorm:"type:uuid" json:"bucket_id"` 14 | } 15 | 16 | func NewObject( 17 | id string, 18 | name string, 19 | extension string, 20 | bucketID string, 21 | ) (*Object, error) { 22 | if len(strings.TrimSpace(id)) == 0 { 23 | return nil, errors.New("id should not be empty") 24 | } 25 | 26 | if _, err := uuid.Parse(id); err != nil { 27 | return nil, errors.New("id should be a uuid") 28 | } 29 | 30 | if len(strings.TrimSpace(name)) == 0 { 31 | return nil, errors.New("name should not be empty") 32 | } 33 | 34 | if len(strings.TrimSpace(bucketID)) == 0 { 35 | return nil, errors.New("bucket id should not be empty") 36 | } 37 | 38 | if _, err := uuid.Parse(bucketID); err != nil { 39 | return nil, errors.New("bucket id should be a uuid") 40 | } 41 | 42 | if len(strings.TrimSpace(extension)) == 0 && strings.TrimSpace(extension) != "." { 43 | return nil, errors.New("extension should not be empty") 44 | } 45 | 46 | return &Object{ 47 | ID: id, 48 | Name: name, 49 | Extension: extension, 50 | BucketID: bucketID, 51 | }, nil 52 | } 53 | -------------------------------------------------------------------------------- /internal/domain/entities/object_test.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewObject(t *testing.T) { 10 | objectID := uuid.New().String() 11 | name := "object_test" 12 | extension := ".png" 13 | bucketID := uuid.New().String() 14 | 15 | object, err := NewObject( 16 | objectID, 17 | name, 18 | extension, 19 | bucketID, 20 | ) 21 | 22 | assert.Nil(t, err) 23 | assert.NotNil(t, object) 24 | assert.Equal(t, objectID, object.ID) 25 | assert.Equal(t, name, object.Name) 26 | assert.Equal(t, extension, object.Extension) 27 | assert.Equal(t, bucketID, object.BucketID) 28 | } 29 | 30 | func TestNewObjectWithEmptyIDFails(t *testing.T) { 31 | objectID := "" 32 | name := "object_test" 33 | extension := ".png" 34 | bucketID := uuid.New().String() 35 | 36 | object, err := NewObject( 37 | objectID, 38 | name, 39 | extension, 40 | bucketID, 41 | ) 42 | 43 | assert.NotNil(t, err) 44 | assert.Nil(t, object) 45 | } 46 | 47 | func TestNewObjectWithEmptyNameFails(t *testing.T) { 48 | objectID := uuid.New().String() 49 | name := "" 50 | extension := ".png" 51 | bucketID := uuid.New().String() 52 | 53 | object, err := NewObject( 54 | objectID, 55 | name, 56 | extension, 57 | bucketID, 58 | ) 59 | 60 | assert.NotNil(t, err) 61 | assert.Nil(t, object) 62 | } 63 | 64 | func TestNewObjectWithEmptyExtensionFails(t *testing.T) { 65 | objectID := uuid.New().String() 66 | name := "object_test" 67 | extension := "" 68 | bucketID := uuid.New().String() 69 | 70 | object, err := NewObject( 71 | objectID, 72 | name, 73 | extension, 74 | bucketID, 75 | ) 76 | 77 | assert.NotNil(t, err) 78 | assert.Nil(t, object) 79 | } 80 | 81 | func TestNewObjectWithInvalidIDFails(t *testing.T) { 82 | objectID := "random_id" 83 | name := "object_test" 84 | extension := ".png" 85 | bucketID := uuid.New().String() 86 | 87 | object, err := NewObject( 88 | objectID, 89 | name, 90 | extension, 91 | bucketID, 92 | ) 93 | 94 | assert.NotNil(t, err) 95 | assert.Nil(t, object) 96 | } 97 | 98 | func TestNewObjectWithInvalidBucketIDFails(t *testing.T) { 99 | objectID := uuid.New().String() 100 | name := "object_test" 101 | extension := ".png" 102 | bucketID := "random_id" 103 | 104 | object, err := NewObject( 105 | objectID, 106 | name, 107 | extension, 108 | bucketID, 109 | ) 110 | 111 | assert.NotNil(t, err) 112 | assert.Nil(t, object) 113 | } 114 | -------------------------------------------------------------------------------- /internal/domain/repositories/bucket_respository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/domain/entities" 5 | ) 6 | 7 | type BucketRepository interface { 8 | Save(bucket entities.Bucket) error 9 | GetAll() ([]entities.Bucket, error) 10 | FindByID(ID string) (*entities.Bucket, error) 11 | GetByParentID(parentID string) ([]entities.Bucket, error) 12 | DeleteByID(ID string) error 13 | } 14 | -------------------------------------------------------------------------------- /internal/domain/repositories/object_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/domain/entities" 5 | ) 6 | 7 | type ObjectRepository interface { 8 | Save(object entities.Object) error 9 | FindByID(ID string) (*entities.Object, error) 10 | GetByBucketID(bucketID string) ([]entities.Object, error) 11 | DeleteByID(objectID string) error 12 | } 13 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/create_bucket.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type CreateBucket struct { 11 | createBucketService *application.CreateBucketService 12 | } 13 | 14 | type CreateBucketRequest struct { 15 | Name string `form:"name" json:"name" xml:"name"` 16 | ParentID *string `form:"parent_id" json:"parent_id" xml:"parent_id"` 17 | } 18 | 19 | func NewCreateBucket( 20 | createBucketService *application.CreateBucketService, 21 | ) *CreateBucket { 22 | return &CreateBucket{ 23 | createBucketService, 24 | } 25 | } 26 | 27 | func (ctrl *CreateBucket) Handle(c echo.Context) error { 28 | request := CreateBucketRequest{} 29 | 30 | err := (&echo.DefaultBinder{}).BindBody(c, &request) 31 | if err != nil { 32 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 33 | } 34 | 35 | bucket, err := ctrl.createBucketService.Do(request.Name, request.ParentID) 36 | if err != nil { 37 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 38 | } 39 | 40 | return c.JSON(http.StatusCreated, bucket) 41 | } 42 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/create_object.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type CreateObject struct { 11 | createObjectService *application.CreateObjectService 12 | } 13 | 14 | type CreateObjectParams struct { 15 | BucketID string `param:"bucketID"` 16 | } 17 | 18 | func NewCreateObject( 19 | createObjectService *application.CreateObjectService, 20 | ) *CreateObject { 21 | return &CreateObject{ 22 | createObjectService, 23 | } 24 | } 25 | 26 | func (ctrl *CreateObject) Handle(c echo.Context) error { 27 | params := CreateObjectParams{} 28 | err := (&echo.DefaultBinder{}).BindPathParams(c, ¶ms) 29 | if err != nil { 30 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 31 | } 32 | 33 | file, err := c.FormFile("file") 34 | if err != nil { 35 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 36 | } 37 | 38 | object, err := ctrl.createObjectService.Do(file, params.BucketID) 39 | if err != nil { 40 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 41 | } 42 | 43 | return c.JSON(http.StatusCreated, object) 44 | } 45 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/delete_bucket.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type DeleteBucket struct { 11 | deleteBucketService application.DeleteBucketServiceInputPort 12 | } 13 | 14 | type DeleteBucketParams struct { 15 | BucketID string `param:"bucketID"` 16 | } 17 | 18 | func NewDeleteBucket( 19 | deleteBucketService application.DeleteBucketServiceInputPort, 20 | ) *DeleteBucket { 21 | return &DeleteBucket{ 22 | deleteBucketService, 23 | } 24 | } 25 | 26 | func (ctrl *DeleteBucket) Handle(c echo.Context) error { 27 | params := DeleteBucketParams{} 28 | err := (&echo.DefaultBinder{}).BindPathParams(c, ¶ms) 29 | if err != nil { 30 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 31 | } 32 | 33 | err = ctrl.deleteBucketService.Do(params.BucketID) 34 | if err != nil { 35 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 36 | } 37 | 38 | return c.JSON(http.StatusOK, dtos.NewMessage("bucket deleted")) 39 | } 40 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/delete_object.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type DeleteObject struct { 11 | deleteObjectService application.DeleteObjectServiceInputPort 12 | } 13 | 14 | type DeleteObjectParams struct { 15 | ObjectID string `param:"objectID"` 16 | } 17 | 18 | func NewDeleteObject( 19 | deleteObjectService application.DeleteObjectServiceInputPort, 20 | ) *DeleteObject { 21 | return &DeleteObject{ 22 | deleteObjectService, 23 | } 24 | } 25 | 26 | func (ctrl *DeleteObject) Handle(c echo.Context) error { 27 | params := DeleteObjectParams{} 28 | err := (&echo.DefaultBinder{}).BindPathParams(c, ¶ms) 29 | if err != nil { 30 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 31 | } 32 | 33 | err = ctrl.deleteObjectService.Do(params.ObjectID) 34 | if err != nil { 35 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 36 | } 37 | 38 | return c.JSON(http.StatusOK, dtos.NewMessage("object deleted")) 39 | } 40 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/download_object.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/jibaru/gostore/internal/shared" 7 | "github.com/labstack/echo/v4" 8 | "net/http" 9 | ) 10 | 11 | type DownloadObject struct { 12 | urlGenerator *shared.UrlGenerator 13 | generateObjectPathService application.GenerateObjectPathServiceInputPort 14 | } 15 | 16 | type DownloadObjectParams struct { 17 | ObjectID string `param:"objectID"` 18 | } 19 | 20 | func NewDownloadObject( 21 | urlGenerator *shared.UrlGenerator, 22 | generateObjectPathService application.GenerateObjectPathServiceInputPort, 23 | ) *DownloadObject { 24 | return &DownloadObject{ 25 | urlGenerator, 26 | generateObjectPathService, 27 | } 28 | } 29 | 30 | func (ctrl *DownloadObject) Handle(c echo.Context) error { 31 | params := DownloadObjectParams{} 32 | err := (&echo.DefaultBinder{}).BindPathParams(c, ¶ms) 33 | if err != nil { 34 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 35 | } 36 | 37 | path, err := ctrl.generateObjectPathService.Do(params.ObjectID) 38 | if err != nil { 39 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 40 | } 41 | 42 | return c.Redirect(http.StatusSeeOther, ctrl.urlGenerator.GenerateUrlFromObjectPath(path)) 43 | } 44 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/dtos/message.go: -------------------------------------------------------------------------------- 1 | package dtos 2 | 3 | type Message struct { 4 | Message string `json:"message"` 5 | } 6 | 7 | func NewMessage(content string) *Message { 8 | return &Message{content} 9 | } 10 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/get_buckets.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type GetBuckets struct { 11 | getBucketsService application.GetBucketServiceInputPort 12 | } 13 | 14 | func NewGetBuckets( 15 | getBucketsService application.GetBucketServiceInputPort, 16 | ) *GetBuckets { 17 | return &GetBuckets{ 18 | getBucketsService, 19 | } 20 | } 21 | 22 | func (ctrl *GetBuckets) Handle(c echo.Context) error { 23 | buckets, err := ctrl.getBucketsService.Do() 24 | if err != nil { 25 | return c.JSON(http.StatusInternalServerError, dtos.NewMessage(err.Error())) 26 | } 27 | 28 | return c.JSON(http.StatusOK, buckets) 29 | } 30 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/get_buckets_in_bucket.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type GetBucketsInBucket struct { 11 | getBucketsInBucketService application.GetBucketsInBucketServiceInputPort 12 | } 13 | 14 | type GetBucketsInBucketParams struct { 15 | BucketID string `param:"bucketID"` 16 | } 17 | 18 | func NewGetBucketsInBucket( 19 | getBucketsInBucketService application.GetBucketsInBucketServiceInputPort, 20 | ) *GetBucketsInBucket { 21 | return &GetBucketsInBucket{ 22 | getBucketsInBucketService, 23 | } 24 | } 25 | 26 | func (ctrl *GetBucketsInBucket) Handle(c echo.Context) error { 27 | params := GetBucketsInBucketParams{} 28 | err := (&echo.DefaultBinder{}).BindPathParams(c, ¶ms) 29 | if err != nil { 30 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 31 | } 32 | 33 | buckets, err := ctrl.getBucketsInBucketService.Do(params.BucketID) 34 | if err != nil { 35 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 36 | } 37 | 38 | return c.JSON(http.StatusOK, buckets) 39 | } 40 | -------------------------------------------------------------------------------- /internal/infrastructure/controllers/get_objects_in_bucket.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/jibaru/gostore/internal/application" 5 | "github.com/jibaru/gostore/internal/infrastructure/controllers/dtos" 6 | "github.com/labstack/echo/v4" 7 | "net/http" 8 | ) 9 | 10 | type GetObjectsInBucket struct { 11 | getObjectsInBucketService application.GetObjectsInBucketServiceInputPort 12 | } 13 | 14 | type GetObjectsInBucketParams struct { 15 | BucketID string `param:"bucketID"` 16 | } 17 | 18 | func NewGetObjectsInBucket( 19 | getObjectsInBucketService application.GetObjectsInBucketServiceInputPort, 20 | ) *GetObjectsInBucket { 21 | return &GetObjectsInBucket{ 22 | getObjectsInBucketService, 23 | } 24 | } 25 | 26 | func (ctrl *GetObjectsInBucket) Handle(c echo.Context) error { 27 | params := GetObjectsInBucketParams{} 28 | err := (&echo.DefaultBinder{}).BindPathParams(c, ¶ms) 29 | if err != nil { 30 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 31 | } 32 | 33 | objects, err := ctrl.getObjectsInBucketService.Do(params.BucketID) 34 | if err != nil { 35 | return c.JSON(http.StatusBadRequest, dtos.NewMessage(err.Error())) 36 | } 37 | 38 | return c.JSON(http.StatusOK, objects) 39 | } 40 | -------------------------------------------------------------------------------- /internal/infrastructure/repositories/file_bucket_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/jibaru/gostore/internal/domain/entities" 7 | "os" 8 | ) 9 | 10 | type FileBucketRepository struct { 11 | filePath string 12 | buckets []entities.Bucket 13 | } 14 | 15 | func NewFileBucketRepository(filePath string) (*FileBucketRepository, error) { 16 | repo := &FileBucketRepository{ 17 | filePath: filePath, 18 | } 19 | 20 | err := repo.loadFromJSONFile() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return repo, nil 26 | } 27 | 28 | func (r *FileBucketRepository) Save(bucket entities.Bucket) error { 29 | r.buckets = append(r.buckets, bucket) 30 | return r.saveToJSONFile() 31 | } 32 | 33 | func (r *FileBucketRepository) GetAll() ([]entities.Bucket, error) { 34 | return r.buckets, nil 35 | } 36 | 37 | func (r *FileBucketRepository) FindByID(ID string) (*entities.Bucket, error) { 38 | for _, bucket := range r.buckets { 39 | if bucket.ID == ID { 40 | return &bucket, nil 41 | } 42 | } 43 | 44 | return nil, errors.New("bucket not found") 45 | } 46 | 47 | func (r *FileBucketRepository) GetByParentID(parentID string) ([]entities.Bucket, error) { 48 | buckets := make([]entities.Bucket, 0) 49 | 50 | for _, bucket := range r.buckets { 51 | if bucket.ParentID != nil && *bucket.ParentID == parentID { 52 | buckets = append(buckets, bucket) 53 | } 54 | } 55 | 56 | return buckets, nil 57 | } 58 | 59 | func (r *FileBucketRepository) DeleteByID(ID string) error { 60 | buckets := make([]entities.Bucket, 0) 61 | 62 | for _, bucket := range r.buckets { 63 | if bucket.ID != ID { 64 | buckets = append(buckets, bucket) 65 | } 66 | } 67 | 68 | r.buckets = buckets 69 | 70 | err := r.saveToJSONFile() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (r *FileBucketRepository) loadFromJSONFile() error { 79 | if _, err := os.Stat(r.filePath); os.IsNotExist(err) { 80 | r.buckets = []entities.Bucket{} 81 | return nil 82 | } 83 | 84 | data, err := os.ReadFile(r.filePath) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | err = json.Unmarshal(data, &r.buckets) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (r *FileBucketRepository) saveToJSONFile() error { 98 | data, err := json.Marshal(r.buckets) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | err = os.WriteFile(r.filePath, data, 0644) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/infrastructure/repositories/file_bucket_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/google/uuid" 6 | "github.com/jibaru/gostore/internal/domain/entities" 7 | "github.com/stretchr/testify/assert" 8 | "log" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | const testingStorageBucketPath string = "../../../storage/buckets_test.json" 14 | 15 | func setUpFileBucketRepositoryTest() { 16 | err := os.Remove(testingStorageBucketPath) 17 | if err != nil { 18 | log.Println(err.Error()) 19 | } 20 | } 21 | 22 | func mockBuckets(buckets []entities.Bucket) error { 23 | data, err := json.Marshal(buckets) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | err = os.WriteFile(testingStorageBucketPath, data, 0644) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func TestCreateNewFileBucketPathRepository(t *testing.T) { 37 | setUpFileBucketRepositoryTest() 38 | repository, err := NewFileBucketRepository(testingStorageBucketPath) 39 | 40 | assert.Nil(t, err) 41 | assert.NotNil(t, repository) 42 | } 43 | 44 | func TestFileBucketRepositorySave(t *testing.T) { 45 | setUpFileBucketRepositoryTest() 46 | repository, err := NewFileBucketRepository(testingStorageBucketPath) 47 | assert.Nil(t, err) 48 | assert.NotNil(t, repository) 49 | 50 | err = repository.Save(entities.Bucket{ 51 | ID: uuid.New().String(), 52 | Name: "test_bucket", 53 | ParentID: nil, 54 | }) 55 | 56 | assert.Nil(t, err) 57 | } 58 | 59 | func TestFileBucketRepositoryGetAll(t *testing.T) { 60 | firstBucketID := uuid.New().String() 61 | mockedBuckets := []entities.Bucket{ 62 | {ID: firstBucketID, Name: "test_bucket_1", ParentID: nil}, 63 | {ID: uuid.New().String(), Name: "test_bucket_2", ParentID: &firstBucketID}, 64 | } 65 | 66 | err := mockBuckets(mockedBuckets) 67 | assert.Nil(t, err) 68 | 69 | repository, err := NewFileBucketRepository(testingStorageBucketPath) 70 | assert.Nil(t, err) 71 | assert.NotNil(t, repository) 72 | 73 | buckets, err := repository.GetAll() 74 | 75 | assert.Nil(t, err) 76 | assert.Len(t, buckets, len(mockedBuckets)) 77 | assert.Equal(t, mockedBuckets, buckets) 78 | } 79 | 80 | func TestFileBucketRepositoryFindByID(t *testing.T) { 81 | firstBucketID := uuid.New().String() 82 | mockedBuckets := []entities.Bucket{ 83 | {ID: firstBucketID, Name: "test_bucket_1", ParentID: nil}, 84 | {ID: uuid.New().String(), Name: "test_bucket_2", ParentID: &firstBucketID}, 85 | } 86 | 87 | err := mockBuckets(mockedBuckets) 88 | assert.Nil(t, err) 89 | 90 | repository, err := NewFileBucketRepository(testingStorageBucketPath) 91 | assert.Nil(t, err) 92 | assert.NotNil(t, repository) 93 | 94 | bucket, err := repository.FindByID(firstBucketID) 95 | 96 | assert.Nil(t, err) 97 | assert.NotNil(t, bucket) 98 | assert.Equal(t, firstBucketID, bucket.ID) 99 | assert.Equal(t, mockedBuckets[0], *bucket) 100 | } 101 | -------------------------------------------------------------------------------- /internal/infrastructure/repositories/file_object_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/jibaru/gostore/internal/domain/entities" 7 | "os" 8 | ) 9 | 10 | type FileObjectRepository struct { 11 | filePath string 12 | objects []entities.Object 13 | } 14 | 15 | func NewFileObjectRepository(filePath string) (*FileObjectRepository, error) { 16 | repo := &FileObjectRepository{ 17 | filePath: filePath, 18 | } 19 | 20 | err := repo.loadFromJSONFile() 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | return repo, nil 26 | } 27 | 28 | func (r *FileObjectRepository) Save(object entities.Object) error { 29 | r.objects = append(r.objects, object) 30 | return r.saveToJSONFile() 31 | } 32 | 33 | func (r *FileObjectRepository) FindByID(ID string) (*entities.Object, error) { 34 | for _, object := range r.objects { 35 | if object.ID == ID { 36 | return &object, nil 37 | } 38 | } 39 | 40 | return nil, errors.New("object not found") 41 | } 42 | 43 | func (r *FileObjectRepository) GetByBucketID(bucketID string) ([]entities.Object, error) { 44 | objects := make([]entities.Object, 0) 45 | 46 | for _, object := range r.objects { 47 | if object.BucketID == bucketID { 48 | objects = append(objects, object) 49 | } 50 | } 51 | 52 | return objects, nil 53 | } 54 | 55 | func (r *FileObjectRepository) DeleteByID(objectID string) error { 56 | objects := make([]entities.Object, 0) 57 | objectDeleted := false 58 | 59 | for _, object := range r.objects { 60 | if object.ID != objectID { 61 | objects = append(objects, object) 62 | } else { 63 | objectDeleted = true 64 | } 65 | } 66 | 67 | if !objectDeleted { 68 | return errors.New("object not found") 69 | } 70 | 71 | r.objects = objects 72 | 73 | return r.saveToJSONFile() 74 | } 75 | 76 | func (r *FileObjectRepository) loadFromJSONFile() error { 77 | if _, err := os.Stat(r.filePath); os.IsNotExist(err) { 78 | r.objects = []entities.Object{} 79 | return nil 80 | } 81 | 82 | data, err := os.ReadFile(r.filePath) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | err = json.Unmarshal(data, &r.objects) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (r *FileObjectRepository) saveToJSONFile() error { 96 | data, err := json.Marshal(r.objects) 97 | if err != nil { 98 | return err 99 | } 100 | 101 | err = os.WriteFile(r.filePath, data, 0644) 102 | if err != nil { 103 | return err 104 | } 105 | 106 | return nil 107 | } 108 | -------------------------------------------------------------------------------- /internal/infrastructure/repositories/file_object_repository_test.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/google/uuid" 6 | "github.com/jibaru/gostore/internal/domain/entities" 7 | "github.com/stretchr/testify/assert" 8 | "log" 9 | "os" 10 | "testing" 11 | ) 12 | 13 | const testingStorageObjectPath string = "../../../storage/objects_test.json" 14 | 15 | func setUpFileObjectRepositoryTest() { 16 | err := os.Remove(testingStorageObjectPath) 17 | if err != nil { 18 | log.Println(err.Error()) 19 | } 20 | } 21 | 22 | func mockObjects(objects []entities.Object) error { 23 | data, err := json.Marshal(objects) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | err = os.WriteFile(testingStorageObjectPath, data, 0644) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func TestCreateNewFileObjectRepository(t *testing.T) { 37 | setUpFileObjectRepositoryTest() 38 | repository, err := NewFileObjectRepository(testingStorageObjectPath) 39 | 40 | assert.Nil(t, err) 41 | assert.NotNil(t, repository) 42 | } 43 | 44 | func TestFileObjectRepositorySave(t *testing.T) { 45 | setUpFileObjectRepositoryTest() 46 | repository, err := NewFileObjectRepository(testingStorageObjectPath) 47 | assert.Nil(t, err) 48 | assert.NotNil(t, repository) 49 | 50 | err = repository.Save(entities.Object{ 51 | ID: uuid.New().String(), 52 | Name: "test_object", 53 | Extension: ".png", 54 | BucketID: uuid.New().String(), 55 | }) 56 | 57 | assert.Nil(t, err) 58 | } 59 | 60 | func TestFileObjectRepositoryFindByID(t *testing.T) { 61 | mockedObjects := []entities.Object{ 62 | {ID: uuid.New().String(), Name: "test_object_1", Extension: ".png", BucketID: uuid.New().String()}, 63 | {ID: uuid.New().String(), Name: "test_object_2", Extension: ".png", BucketID: uuid.New().String()}, 64 | } 65 | 66 | err := mockObjects(mockedObjects) 67 | assert.Nil(t, err) 68 | 69 | repository, err := NewFileObjectRepository(testingStorageObjectPath) 70 | assert.Nil(t, err) 71 | assert.NotNil(t, repository) 72 | 73 | object, err := repository.FindByID(mockedObjects[0].ID) 74 | 75 | assert.Nil(t, err) 76 | assert.NotNil(t, object) 77 | assert.Equal(t, mockedObjects[0].ID, object.ID) 78 | assert.Equal(t, mockedObjects[0], *object) 79 | } 80 | -------------------------------------------------------------------------------- /internal/infrastructure/repositories/ram_bucket_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "errors" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | "github.com/jibaru/gostore/internal/domain/repositories" 7 | ) 8 | 9 | type RamBucketRepository struct { 10 | buckets []entities.Bucket 11 | } 12 | 13 | func NewEmptyRamBucketRepository() repositories.BucketRepository { 14 | return &RamBucketRepository{ 15 | make([]entities.Bucket, 0), 16 | } 17 | } 18 | 19 | func NewRamBucketRepository(buckets []entities.Bucket) repositories.BucketRepository { 20 | return &RamBucketRepository{ 21 | buckets, 22 | } 23 | } 24 | 25 | func (r *RamBucketRepository) Save(bucket entities.Bucket) error { 26 | r.buckets = append(r.buckets, bucket) 27 | return nil 28 | } 29 | 30 | func (r *RamBucketRepository) GetAll() ([]entities.Bucket, error) { 31 | return r.buckets, nil 32 | } 33 | 34 | func (r *RamBucketRepository) FindByID(ID string) (*entities.Bucket, error) { 35 | for _, bucket := range r.buckets { 36 | if bucket.ID == ID { 37 | return &bucket, nil 38 | } 39 | } 40 | 41 | return nil, errors.New("bucket not found") 42 | } 43 | 44 | func (r *RamBucketRepository) GetByParentID(parentID string) ([]entities.Bucket, error) { 45 | buckets := make([]entities.Bucket, 0) 46 | 47 | for _, bucket := range r.buckets { 48 | if bucket.ParentID != nil && *bucket.ParentID == parentID { 49 | buckets = append(buckets, bucket) 50 | } 51 | } 52 | 53 | return buckets, nil 54 | } 55 | 56 | func (r *RamBucketRepository) DeleteByID(ID string) error { 57 | buckets := make([]entities.Bucket, 0) 58 | 59 | for _, bucket := range r.buckets { 60 | if bucket.ID != ID { 61 | if bucket.ParentID != nil && *bucket.ParentID != ID { 62 | buckets = append(buckets, bucket) 63 | } 64 | } 65 | } 66 | 67 | r.buckets = buckets 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/infrastructure/repositories/ram_object_repository.go: -------------------------------------------------------------------------------- 1 | package repositories 2 | 3 | import ( 4 | "errors" 5 | "github.com/jibaru/gostore/internal/domain/entities" 6 | ) 7 | 8 | type RamObjectRepository struct { 9 | objects []entities.Object 10 | } 11 | 12 | func NewRamObjectRepository(objects []entities.Object) *RamObjectRepository { 13 | return &RamObjectRepository{objects: objects} 14 | } 15 | 16 | func (r *RamObjectRepository) Save(object entities.Object) error { 17 | r.objects = append(r.objects, object) 18 | return nil 19 | } 20 | 21 | func (r *RamObjectRepository) FindByID(ID string) (*entities.Object, error) { 22 | for _, object := range r.objects { 23 | if object.ID == ID { 24 | return &object, nil 25 | } 26 | } 27 | 28 | return nil, errors.New("object not found") 29 | } 30 | 31 | func (r *RamObjectRepository) GetByBucketID(bucketID string) ([]entities.Object, error) { 32 | objects := make([]entities.Object, 0) 33 | 34 | for _, object := range r.objects { 35 | if object.BucketID == bucketID { 36 | objects = append(objects, object) 37 | } 38 | } 39 | 40 | return objects, nil 41 | } 42 | 43 | func (r *RamObjectRepository) DeleteByID(objectID string) error { 44 | objects := make([]entities.Object, 0) 45 | objectDeleted := false 46 | 47 | for _, object := range r.objects { 48 | if object.ID != objectID { 49 | objects = append(objects, object) 50 | } else { 51 | objectDeleted = true 52 | } 53 | } 54 | 55 | if !objectDeleted { 56 | return errors.New("object not found") 57 | } 58 | 59 | r.objects = objects 60 | 61 | return nil 62 | } 63 | 64 | func (r *RamObjectRepository) Size() int { 65 | return len(r.objects) 66 | } 67 | -------------------------------------------------------------------------------- /internal/shared/filesystem.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | type Filesystem interface { 10 | MakeDirectory(name string) error 11 | MakeDirectoryOnPath(name string, relativePath string) error 12 | MakeFileOnPath(name string, relativePath string) (*os.File, error) 13 | DeleteFileOnPath(path string) error 14 | DeleteDirectoryOnPath(relativePath string) error 15 | } 16 | 17 | type ServerFilesystem struct { 18 | rootPath string 19 | } 20 | 21 | type DummyFilesystem struct { 22 | } 23 | 24 | func NewServerFilesystem(rootPath string) Filesystem { 25 | return &ServerFilesystem{ 26 | rootPath, 27 | } 28 | } 29 | 30 | func NewDummyFilesystem() Filesystem { 31 | return &DummyFilesystem{} 32 | } 33 | 34 | func (s *ServerFilesystem) MakeDirectory(name string) error { 35 | if len(strings.TrimSpace(name)) == 0 { 36 | return errors.New("name should be not empty") 37 | } 38 | 39 | return os.Mkdir(s.rootPath+"/"+name, os.ModePerm) 40 | } 41 | 42 | func (s *ServerFilesystem) MakeDirectoryOnPath(name string, relativePath string) error { 43 | return os.Mkdir(s.rootPath+relativePath+"/"+name, os.ModePerm) 44 | } 45 | 46 | func (s *ServerFilesystem) MakeFileOnPath(name string, relativePath string) (*os.File, error) { 47 | return os.Create(s.rootPath + relativePath + "/" + name) 48 | } 49 | 50 | func (s *ServerFilesystem) DeleteFileOnPath(relativePath string) error { 51 | return os.Remove(s.rootPath + relativePath) 52 | } 53 | 54 | func (s *ServerFilesystem) DeleteDirectoryOnPath(relativePath string) error { 55 | return os.RemoveAll(s.rootPath + relativePath) 56 | } 57 | 58 | func (s *DummyFilesystem) MakeDirectory(name string) error { 59 | return nil 60 | } 61 | 62 | func (s *DummyFilesystem) MakeDirectoryOnPath(name string, relativePath string) error { 63 | return nil 64 | } 65 | 66 | func (s *DummyFilesystem) MakeFileOnPath(name string, relativePath string) (*os.File, error) { 67 | return &os.File{}, nil 68 | } 69 | 70 | func (s *DummyFilesystem) DeleteFileOnPath(relativePath string) error { 71 | return nil 72 | } 73 | 74 | func (s *DummyFilesystem) DeleteDirectoryOnPath(relativePath string) error { 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /internal/shared/filesystem_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestMakeDirectoryOnServerFilesystem(t *testing.T) { 10 | serverFilesystem := NewServerFilesystem("../../storage") 11 | 12 | err := serverFilesystem.MakeDirectory(uuid.New().String()) 13 | 14 | assert.Nil(t, err) 15 | } 16 | 17 | func TestMakeDirectoryWithEmptyNameOnServerFilesystemFails(t *testing.T) { 18 | serverFilesystem := NewServerFilesystem("../../storage") 19 | 20 | err := serverFilesystem.MakeDirectory("") 21 | 22 | assert.NotNil(t, err) 23 | } 24 | 25 | func TestMakeFileOnPathOnServerFilesystem(t *testing.T) { 26 | serverFilesystem := NewServerFilesystem("../../storage") 27 | 28 | file, err := serverFilesystem.MakeFileOnPath(uuid.New().String()+".png", "/") 29 | 30 | assert.NotNil(t, file) 31 | assert.Nil(t, err) 32 | } 33 | 34 | func TestServerFilesystem_DeleteFileOnPath(t *testing.T) { 35 | serverFilesystem := NewServerFilesystem("../../storage") 36 | fileName := uuid.New().String() + ".png" 37 | file, err := serverFilesystem.MakeFileOnPath(fileName, "/") 38 | assert.Nil(t, err) 39 | assert.NotNil(t, file) 40 | 41 | err = serverFilesystem.DeleteFileOnPath("/" + fileName) 42 | 43 | assert.Nil(t, err) 44 | } 45 | 46 | func TestServerFilesystem_DeleteDirectoryOnPath(t *testing.T) { 47 | serverFilesystem := NewServerFilesystem("../../storage") 48 | dirName := uuid.New().String() 49 | err := serverFilesystem.MakeDirectoryOnPath(dirName, "/") 50 | assert.Nil(t, err) 51 | 52 | err = serverFilesystem.DeleteDirectoryOnPath("/" + dirName) 53 | 54 | assert.Nil(t, err) 55 | } 56 | -------------------------------------------------------------------------------- /internal/shared/url_generator.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import "strconv" 4 | 5 | type UrlGenerator struct { 6 | host string 7 | port uint 8 | storageFolderName string 9 | useHttps bool 10 | } 11 | 12 | func NewUrlGenerator( 13 | host string, 14 | port uint, 15 | storageFolderName string, 16 | useHttps bool, 17 | ) *UrlGenerator { 18 | return &UrlGenerator{ 19 | host, 20 | port, 21 | storageFolderName, 22 | useHttps, 23 | } 24 | } 25 | 26 | func (that *UrlGenerator) GenerateUrlFromObjectPath(objectPath string) string { 27 | httpPath := "http://" 28 | if that.useHttps { 29 | httpPath = "https://" 30 | } 31 | 32 | return httpPath + that.host + ":" + strconv.Itoa(int(that.port)) + "/" + that.storageFolderName + objectPath 33 | } 34 | -------------------------------------------------------------------------------- /internal/shared/url_generator_test.go: -------------------------------------------------------------------------------- 1 | package shared 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestGenerateHttpUrl(t *testing.T) { 9 | urlGenerator := NewUrlGenerator( 10 | "localhost", 11 | 80, 12 | "storage", 13 | false, 14 | ) 15 | 16 | expectedUrl := "http://localhost:80/storage/folder-name/object.png" 17 | actualUrl := urlGenerator.GenerateUrlFromObjectPath("/folder-name/object.png") 18 | 19 | assert.Equal(t, expectedUrl, actualUrl) 20 | } 21 | 22 | func TestGenerateHttpsUrl(t *testing.T) { 23 | urlGenerator := NewUrlGenerator( 24 | "localhost", 25 | 80, 26 | "storage", 27 | true, 28 | ) 29 | 30 | expectedUrl := "https://localhost:80/storage/folder-name/object.png" 31 | actualUrl := urlGenerator.GenerateUrlFromObjectPath("/folder-name/object.png") 32 | 33 | assert.Equal(t, expectedUrl, actualUrl) 34 | } 35 | -------------------------------------------------------------------------------- /migrations/001_buckets.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE buckets ( 2 | id CHAR(36) NOT NULL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | parent_id CHAR(36) NULL FOREIGN KEY REFERENCES buckets(id), 5 | ); 6 | -------------------------------------------------------------------------------- /migrations/002_objects.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE objects ( 2 | id CHAR(36) NOT NULL PRIMARY KEY, 3 | name VARCHAR(255) NOT NULL, 4 | bucket_id CHAR(36) NOT NULL FOREIGN KEY REFERENCES buckets(id), 5 | ); 6 | -------------------------------------------------------------------------------- /storage/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Jibaru/gostore/976f222f69312ce48c86942bd3c726e77c49c81d/storage/.gitkeep --------------------------------------------------------------------------------