├── frontend ├── src │ ├── components │ │ ├── drive │ │ │ ├── List.vue │ │ │ └── CreateDrawer.vue │ │ ├── dashboard │ │ │ ├── Update.vue │ │ │ └── Security.vue │ │ ├── ui │ │ │ ├── LabelButton.vue │ │ │ ├── LabelInput.vue │ │ │ └── SlotInput.vue │ │ └── TopNav.vue │ ├── css │ │ ├── _element.scss │ │ ├── color.scss │ │ ├── _transition.scss │ │ ├── _webkit.scss │ │ ├── global.scss │ │ ├── markdown.scss │ │ └── highlight.scss │ ├── main.js │ ├── marked.js │ ├── views │ │ ├── drive │ │ │ ├── util.js │ │ │ ├── Plan.vue │ │ │ ├── Search.vue │ │ │ └── RecycleBin.vue │ │ ├── identity │ │ │ ├── Logout.vue │ │ │ ├── Login.vue │ │ │ ├── Register.vue │ │ │ └── Dashboard.vue │ │ └── Home.vue │ ├── backend.js │ ├── App.vue │ └── router │ │ └── index.js ├── Dockerfile ├── jsconfig.json ├── public │ └── icon │ │ ├── file.png │ │ ├── folder.png │ │ ├── arrow │ │ ├── top.png │ │ ├── left.png │ │ ├── right.png │ │ └── bottom.png │ │ ├── general │ │ └── home.svg │ │ └── top-nav │ │ └── article.svg ├── env-dev.json ├── vite.config.js ├── index.html └── package.json ├── .gitignore ├── backend ├── storage │ ├── usecases │ │ ├── channel_download.go │ │ ├── download.go │ │ └── upload.go │ ├── Dockerfile │ ├── ports │ │ ├── http.go │ │ ├── download.go │ │ └── upload.go │ ├── remote │ │ └── service.go │ ├── repository │ │ └── repository.go │ ├── adapters │ │ ├── capacity_repository_test.go │ │ ├── event_repository_test.go │ │ ├── drive_service.go │ │ ├── object_repository.go │ │ ├── event_repository.go │ │ ├── object_repository_test.go │ │ └── capacity_repository.go │ ├── domain │ │ └── object.go │ ├── main │ │ └── main.go │ ├── go.mod │ └── service │ │ └── service.go ├── pkg │ ├── go.mod │ ├── types │ │ ├── storage.go │ │ ├── user.go │ │ ├── errors.go │ │ └── drive.go │ ├── events │ │ └── events.go │ └── errors │ │ └── errors.go ├── drive │ ├── Dockerfile │ ├── domain │ │ ├── user.go │ │ ├── state.go │ │ ├── folder.go │ │ ├── parent.go │ │ ├── file.go │ │ └── drive.go │ ├── ports │ │ ├── rpc.go │ │ ├── http.go │ │ ├── finish_upload.go │ │ ├── create_drive.go │ │ ├── start_download.go │ │ ├── get_path.go │ │ ├── start_upload.go │ │ ├── share_file.go │ │ ├── remove_file.go │ │ ├── restore_file.go │ │ ├── share_folder.go │ │ ├── remove_folder.go │ │ ├── restore_folder.go │ │ ├── create_folder.go │ │ ├── get_file.go │ │ ├── get_folder.go │ │ ├── get_recycle_bin.go │ │ └── get_drive.go │ ├── remote │ │ └── service.go │ ├── usecases │ │ ├── types.go │ │ ├── finish_upload.go │ │ ├── create_drive.go │ │ ├── remove_file.go │ │ ├── restore_file.go │ │ ├── share_file.go │ │ ├── share_folder.go │ │ ├── create_folder.go │ │ ├── get_path.go │ │ ├── delete_file.go │ │ ├── get_recycle_bin.go │ │ ├── start_download.go │ │ ├── get_file.go │ │ ├── move_file.go │ │ ├── move_folder.go │ │ ├── get_drive.go │ │ ├── get_folder.go │ │ └── start_upload.go │ ├── adapters │ │ ├── repository.go │ │ ├── drive_model.go │ │ ├── user_service.go │ │ ├── folder_model.go │ │ ├── file_model.go │ │ └── drive_repository.go │ ├── go.mod │ ├── main │ │ └── main.go │ └── repository │ │ └── repository.go ├── user │ ├── Dockerfile │ ├── ports │ │ ├── rpc.go │ │ ├── http.go │ │ ├── register.go │ │ ├── login.go │ │ └── get_user.go │ ├── domain │ │ ├── password.go │ │ ├── repository.go │ │ ├── account.go │ │ ├── user.go │ │ └── session.go │ ├── adapters │ │ ├── session_model.go │ │ ├── user_repository.go │ │ ├── user_model.go │ │ └── session_repository.go │ ├── usecases │ │ ├── register.go │ │ ├── login.go │ │ └── get_user.go │ ├── service │ │ ├── service_test.go │ │ └── service.go │ ├── main │ │ └── main.go │ └── go.mod ├── common │ ├── auth │ │ └── cookie.go │ ├── go.mod │ ├── utils │ │ └── random.go │ ├── decorator │ │ └── handler.go │ └── go.sum └── channel │ ├── go.sum │ ├── go.mod │ ├── domain │ ├── channel.go │ ├── message.go │ ├── file.go │ └── member.go │ ├── repository │ └── repository.go │ └── usecases │ ├── list_channel.go │ ├── create_channel.go │ ├── join_channel.go │ ├── find_message.go │ ├── send_message.go │ └── send_file.go ├── docs ├── img │ └── arch.png └── detail.md ├── test ├── src │ ├── small.txt │ ├── utils.js │ └── performance.js ├── package.json └── package-lock.json ├── README.md └── docker-compose.yaml /frontend/src/components/drive/List.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | build/ 3 | node_modules/ 4 | dist/ -------------------------------------------------------------------------------- /backend/storage/usecases/channel_download.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | COPY dist /usr/share/nginx/html 3 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "./src/**/*" 4 | ] 5 | } -------------------------------------------------------------------------------- /frontend/src/css/_element.scss: -------------------------------------------------------------------------------- 1 | @import "element-plus/theme-chalk/index.css"; -------------------------------------------------------------------------------- /backend/pkg/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axli-personal/drive/backend/pkg 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /docs/img/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/docs/img/arch.png -------------------------------------------------------------------------------- /backend/drive/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | WORKDIR /app 3 | COPY ./build . 4 | CMD ["./main"] 5 | -------------------------------------------------------------------------------- /backend/storage/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | WORKDIR /app 3 | COPY ./build . 4 | CMD ["./main"] 5 | -------------------------------------------------------------------------------- /backend/user/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | WORKDIR /app 3 | COPY ./build . 4 | CMD ["./main"] 5 | -------------------------------------------------------------------------------- /backend/common/auth/cookie.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | const ( 4 | SessionIdCookieKey = "SID" 5 | ) 6 | -------------------------------------------------------------------------------- /frontend/public/icon/file.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/frontend/public/icon/file.png -------------------------------------------------------------------------------- /frontend/public/icon/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/frontend/public/icon/folder.png -------------------------------------------------------------------------------- /frontend/public/icon/arrow/top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/frontend/public/icon/arrow/top.png -------------------------------------------------------------------------------- /frontend/public/icon/arrow/left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/frontend/public/icon/arrow/left.png -------------------------------------------------------------------------------- /frontend/public/icon/arrow/right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/frontend/public/icon/arrow/right.png -------------------------------------------------------------------------------- /frontend/public/icon/arrow/bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axli-personal/drive/HEAD/frontend/public/icon/arrow/bottom.png -------------------------------------------------------------------------------- /frontend/src/css/color.scss: -------------------------------------------------------------------------------- 1 | .gray-hover:hover { 2 | background: #DFDADA; 3 | } 4 | 5 | .blue-background { 6 | background: #00a1d7; 7 | } -------------------------------------------------------------------------------- /frontend/src/css/_transition.scss: -------------------------------------------------------------------------------- 1 | /* Transition */ 2 | .fade-enter-from { 3 | opacity: 0; 4 | } 5 | 6 | .fade-enter-active { 7 | transition: opacity 1s; 8 | } 9 | -------------------------------------------------------------------------------- /test/src/small.txt: -------------------------------------------------------------------------------- 1 | Hello World 2 | Hello World 3 | Hello World 4 | Hello World 5 | Hello World 6 | Hello World 7 | Hello World 8 | Hello World 9 | Hello World 10 | Hello World -------------------------------------------------------------------------------- /backend/pkg/types/storage.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // HTTP 4 | 5 | type GetObjectRequest struct { 6 | FileId string `params:"fileId"` 7 | Download bool `query:"download"` 8 | } 9 | -------------------------------------------------------------------------------- /frontend/env-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "USER_SERVICE_URL": "http://127.0.0.1:8080", 3 | "DRIVE_SERVICE_URL": "http://127.0.0.1:8081", 4 | "STORAGE_SERVICE_URL": "http://127.0.0.1:8082" 5 | } -------------------------------------------------------------------------------- /backend/channel/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 2 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | -------------------------------------------------------------------------------- /backend/common/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axli-personal/drive/backend/common 2 | 3 | go 1.20 4 | 5 | require github.com/sirupsen/logrus v1.9.0 6 | 7 | require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect 8 | -------------------------------------------------------------------------------- /frontend/src/css/_webkit.scss: -------------------------------------------------------------------------------- 1 | /* Scrollbar */ 2 | ::-webkit-scrollbar { 3 | width: 8px; 4 | height: 8px; 5 | } 6 | 7 | ::-webkit-scrollbar-thumb { 8 | border-radius: 4px; 9 | background: #c151c5; 10 | } 11 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drive-test", 3 | "license": "MIT", 4 | "scripts": { 5 | "test": "k6 run src/performance.js" 6 | }, 7 | "devDependencies": { 8 | "@types/k6": "^0.43.2" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import vue from '@vitejs/plugin-vue'; 3 | 4 | export default defineConfig({ 5 | base: "./", 6 | plugins: [vue()], 7 | build: { 8 | chunkSizeWarningLimit: 1500 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /frontend/src/css/global.scss: -------------------------------------------------------------------------------- 1 | @import "webkit", "transition", "element"; 2 | 3 | /* Document */ 4 | body { 5 | margin: 0; 6 | background: #f7f5f5; 7 | } 8 | 9 | /* Font Family */ 10 | body, input { 11 | font-family: "Fira Code", monospace; 12 | } 13 | -------------------------------------------------------------------------------- /backend/common/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "math/rand" 4 | 5 | func RandomString(length int) string { 6 | s := make([]byte, length) 7 | 8 | for i := 0; i < length; i++ { 9 | s[i] = 'a' + byte(rand.Intn(26)) 10 | } 11 | 12 | return string(s) 13 | } 14 | -------------------------------------------------------------------------------- /backend/drive/domain/user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | type User struct { 4 | account string 5 | } 6 | 7 | func NewUserFromService(account string) User { 8 | return User{account: account} 9 | } 10 | 11 | func (user User) Account() string { 12 | return user.account 13 | } 14 | -------------------------------------------------------------------------------- /backend/user/ports/rpc.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "github.com/axli-personal/drive/backend/user/service" 4 | 5 | type RPCServer struct { 6 | svc service.Service 7 | } 8 | 9 | func NewRPCServer(svc service.Service) RPCServer { 10 | return RPCServer{svc: svc} 11 | } 12 | -------------------------------------------------------------------------------- /backend/drive/ports/rpc.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "github.com/axli-personal/drive/backend/drive/service" 4 | 5 | type RPCServer struct { 6 | svc service.Service 7 | } 8 | 9 | func NewRPCServer(svc service.Service) RPCServer { 10 | return RPCServer{svc: svc} 11 | } 12 | -------------------------------------------------------------------------------- /backend/storage/ports/http.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import "github.com/axli-personal/drive/backend/storage/service" 4 | 5 | type HTTPServer struct { 6 | svc service.Service 7 | } 8 | 9 | func NewHTTPServer(svc service.Service) HTTPServer { 10 | return HTTPServer{svc: svc} 11 | } 12 | -------------------------------------------------------------------------------- /backend/user/ports/http.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/user/service" 5 | ) 6 | 7 | type HTTPServer struct { 8 | svc service.Service 9 | } 10 | 11 | func NewHTTPServer(svc service.Service) HTTPServer { 12 | return HTTPServer{svc: svc} 13 | } 14 | -------------------------------------------------------------------------------- /backend/drive/ports/http.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/service" 5 | ) 6 | 7 | type HTTPServer struct { 8 | svc service.Service 9 | } 10 | 11 | func NewHTTPServer(svc service.Service) HTTPServer { 12 | return HTTPServer{svc: svc} 13 | } 14 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import "./css/global.scss"; 2 | 3 | import { createApp } from "vue"; 4 | import App from "./App.vue"; 5 | import router from "./router"; 6 | import ElementPlus from 'element-plus'; 7 | 8 | const app = createApp(App); 9 | 10 | app.use(router).use(ElementPlus).mount("#app"); 11 | -------------------------------------------------------------------------------- /docs/detail.md: -------------------------------------------------------------------------------- 1 | ## 路径存储 2 | 3 | ### 方案一: 深度 + 完整路径 4 | 5 | ```text 6 | 0/ 7 | 1/home 8 | 2/home/dir 9 | 3/home/dir/file 10 | ``` 11 | 12 | 优点: 便于获取路径. 13 | 14 | > [Baidu File System Design](https://github.com/baidu/bfs/blob/master/docs/cn/design.md#namespace) 15 | 16 | ### 方案二: 文件名 + 父级指针 17 | 18 | 优点: 便于操作目录. 19 | -------------------------------------------------------------------------------- /backend/drive/remote/service.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/drive/domain" 6 | ) 7 | 8 | type ( 9 | UserService interface { 10 | GetUser(ctx context.Context, sessionId string) (domain.User, error) 11 | } 12 | 13 | StorageCluster interface { 14 | ChooseStorageEndPoint(ctx context.Context) (string, error) 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /frontend/src/marked.js: -------------------------------------------------------------------------------- 1 | import { marked } from "marked"; 2 | import hljs from "highlight.js"; 3 | import "/src/css/highlight.scss"; 4 | 5 | marked.setOptions({ 6 | highlight: function (code, lang) { 7 | const language = hljs.getLanguage(lang) ? lang : "plaintext"; 8 | return hljs.highlight(code, { language }).value; 9 | }, 10 | langPrefix: "hljs language-" 11 | }); 12 | 13 | export default marked; -------------------------------------------------------------------------------- /frontend/src/views/drive/util.js: -------------------------------------------------------------------------------- 1 | export function getFileViewType(name) { 2 | let viewType = "binary"; 3 | const dotPos = name.lastIndexOf("."); 4 | if (dotPos !== -1) { 5 | switch (name.substring(dotPos + 1)) { 6 | case "txt": 7 | viewType = "text" 8 | break; 9 | case "md": 10 | viewType = "markdown" 11 | break; 12 | } 13 | } 14 | return viewType; 15 | } -------------------------------------------------------------------------------- /backend/channel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axli-personal/drive/backend/channel 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/axli-personal/drive/backend/common v0.0.0 7 | github.com/axli-personal/drive/backend/pkg v0.0.0 8 | github.com/google/uuid v1.3.0 9 | ) 10 | 11 | replace ( 12 | github.com/axli-personal/drive/backend/common => ../common/ 13 | github.com/axli-personal/drive/backend/pkg => ../pkg/ 14 | ) -------------------------------------------------------------------------------- /test/src/utils.js: -------------------------------------------------------------------------------- 1 | const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" 2 | 3 | export function randomString(length) { 4 | let result = ''; 5 | while (length--) { 6 | result += charset[Math.floor(charset.length * Math.random())]; 7 | } 8 | return result; 9 | } 10 | 11 | export function statusCheck(response) { 12 | return response.status >= 200 && response.status < 300; 13 | } 14 | -------------------------------------------------------------------------------- /backend/storage/remote/service.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/pkg/types" 5 | ) 6 | 7 | type ( 8 | DriveService interface { 9 | StartUpload(request types.StartUploadRequest) (types.StartUploadResponse, error) 10 | StartDownload(request types.StartDownloadRequest) (types.StartDownloadResponse, error) 11 | FinishUpload(request types.FinishUploadRequest) (types.FinishUploadResponse, error) 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /frontend/src/views/identity/Logout.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /backend/user/domain/password.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "errors" 4 | 5 | type Password struct { 6 | value string 7 | } 8 | 9 | func NewPassword(password string) (Password, error) { 10 | if len(password) < 6 || len(password) > 30 { 11 | return Password{}, errors.New("invalid password length") 12 | } 13 | 14 | return Password{value: password}, nil 15 | } 16 | 17 | func (p Password) IsZero() bool { 18 | return p == Password{} 19 | } 20 | 21 | func (p Password) String() string { 22 | return p.value 23 | } 24 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Tech City 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /backend/pkg/types/user.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // RPC 4 | 5 | type GetUserRequest struct { 6 | SessionId string 7 | } 8 | 9 | type GetUserResponse struct { 10 | Account string 11 | Username string 12 | Introduction string 13 | } 14 | 15 | // HTTP 16 | 17 | type LoginRequest struct { 18 | Account string `json:"account"` 19 | Password string `json:"password"` 20 | } 21 | 22 | type RegisterRequest struct { 23 | Account string `json:"account"` 24 | Password string `json:"password"` 25 | Username string `json:"username"` 26 | } 27 | -------------------------------------------------------------------------------- /backend/channel/domain/channel.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Channel struct { 9 | Id uuid.UUID 10 | OwnerAccount string 11 | Name string 12 | InviteToken string 13 | CreatedAt time.Time 14 | } 15 | 16 | func NewChannel(name string, ownerAccount string) (*Channel, error) { 17 | return &Channel{ 18 | Id: uuid.New(), 19 | OwnerAccount: ownerAccount, 20 | Name: name, 21 | InviteToken: uuid.New().String(), 22 | CreatedAt: time.Now(), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drive-web", 3 | "license": "MIT", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "serve": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@element-plus/icons-vue": "^2.1.0", 11 | "axios": "0.26.0", 12 | "element-plus": "^2.3.3", 13 | "highlight.js": "^11.4.0", 14 | "marked": "~4.0.12", 15 | "vue": "~3.2.31", 16 | "vue-router": "~4.0.12" 17 | }, 18 | "devDependencies": { 19 | "@vitejs/plugin-vue": "^2.2.0", 20 | "sass": "^1.49.0", 21 | "vite": "^2.8.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/channel/domain/message.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Message struct { 9 | Id uuid.UUID 10 | ChannelId uuid.UUID 11 | SenderAccount string 12 | Text string 13 | SendAt time.Time 14 | } 15 | 16 | func NewChannelMessage(channelId uuid.UUID, senderAccount string, text string) (*Message, error) { 17 | return &Message{ 18 | Id: uuid.New(), 19 | ChannelId: channelId, 20 | SenderAccount: senderAccount, 21 | Text: text, 22 | SendAt: time.Now(), 23 | }, nil 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/backend.js: -------------------------------------------------------------------------------- 1 | import env from "/env-dev.json"; 2 | import axios from "axios"; 3 | 4 | const site = "http://127.0.0.1:80"; 5 | 6 | const userService = axios.create({ 7 | baseURL: env.USER_SERVICE_URL, 8 | withCredentials: true, 9 | }); 10 | 11 | const driveService = axios.create({ 12 | baseURL: env.DRIVE_SERVICE_URL, 13 | withCredentials: true, 14 | }); 15 | 16 | const storageService = axios.create({ 17 | baseURL: env.STORAGE_SERVICE_URL, 18 | withCredentials: true, 19 | }) 20 | 21 | export { 22 | site, 23 | userService, 24 | driveService, 25 | storageService, 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/views/drive/Plan.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 24 | -------------------------------------------------------------------------------- /test/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drive-test", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "drive-test", 9 | "license": "MIT", 10 | "devDependencies": { 11 | "@types/k6": "^0.43.2" 12 | } 13 | }, 14 | "node_modules/@types/k6": { 15 | "version": "0.43.2", 16 | "resolved": "https://registry.npmjs.org/@types/k6/-/k6-0.43.2.tgz", 17 | "integrity": "sha512-DD24gUZ5Y+tEZZrNt0wW9xZO3U81mWPQWBkcwms+V37GVUeJQpONriKceq5jgcyoORy9Ra6gjXB3wMhMllB+dw==", 18 | "dev": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backend/user/domain/repository.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "context" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | var ( 10 | ErrCodeRepository = "Repository" 11 | ErrCodeNotFound = "NotFound" 12 | ErrCodeUnmarshal = "Unmarshal" 13 | ) 14 | 15 | type ( 16 | SessionRepository interface { 17 | SaveSession(ctx context.Context, session *Session, expire time.Duration) error 18 | 19 | GetSession(ctx context.Context, id uuid.UUID) (*Session, error) 20 | } 21 | 22 | UserRepository interface { 23 | SaveUser(ctx context.Context, user *User) error 24 | 25 | GetUser(ctx context.Context, account Account) (*User, error) 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /backend/drive/ports/finish_upload.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/drive/usecases" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | func (server RPCServer) FinishUpload(request *types.FinishUploadRequest, response *types.FinishUploadResponse) (err error) { 11 | fileId, err := uuid.Parse(request.FileId) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | _, err = server.svc.FinishUpload.Handle( 17 | context.Background(), 18 | usecases.FinishUploadArgs{ 19 | FileId: fileId, 20 | }, 21 | ) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /backend/drive/ports/create_drive.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/drive/usecases" 6 | "github.com/gofiber/fiber/v2" 7 | ) 8 | 9 | func (server HTTPServer) CreateDrive(ctx *fiber.Ctx) (err error) { 10 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 11 | if sessionId == "" { 12 | return ctx.SendStatus(fiber.StatusForbidden) 13 | } 14 | 15 | _, err = server.svc.CreateDrive.Handle( 16 | ctx.Context(), 17 | usecases.CreateDriveArgs{ 18 | SessionId: sessionId, 19 | }, 20 | ) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | return ctx.SendStatus(fiber.StatusOK) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 26 | 27 | 32 | -------------------------------------------------------------------------------- /backend/storage/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/pkg/events" 6 | "github.com/axli-personal/drive/backend/storage/domain" 7 | ) 8 | 9 | type ( 10 | ObjectRepository interface { 11 | SaveObject(ctx context.Context, object *domain.Object) error 12 | GetObject(ctx context.Context, hash string) (*domain.Object, error) 13 | } 14 | 15 | CapacityRepository interface { 16 | DecreaseRequestCapacity(ctx context.Context) error 17 | } 18 | 19 | EventRepository interface { 20 | PublishFileUploaded(ctx context.Context, event events.FileUploaded) error 21 | PublishFileDownloaded(ctx context.Context, event events.FileDownloaded) error 22 | } 23 | ) 24 | -------------------------------------------------------------------------------- /backend/channel/domain/file.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type File struct { 9 | Id uuid.UUID 10 | ChannelId uuid.UUID 11 | SenderAccount string 12 | Name string 13 | Hash string 14 | Size int 15 | SendAt time.Time 16 | } 17 | 18 | func NewChannelFile( 19 | channelId uuid.UUID, 20 | senderAccount string, 21 | name string, 22 | hash string, 23 | size int, 24 | ) (*File, error) { 25 | return &File{ 26 | Id: uuid.New(), 27 | ChannelId: channelId, 28 | SenderAccount: senderAccount, 29 | Name: name, 30 | Hash: hash, 31 | Size: size, 32 | SendAt: time.Now(), 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /backend/pkg/events/events.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | var ( 4 | FieldBody = "Body" 5 | StreamFileUploaded = "file-uploaded" 6 | StreamFileDownloaded = "file-downloaded" 7 | StreamFileDeleted = "file-deleted" 8 | StreamFolderRemoved = "folder-removed" 9 | ) 10 | 11 | type FileUploaded struct { 12 | EventId string `json:"-"` 13 | Endpoint string `json:"endpoint"` 14 | FileId string `json:"fileId"` 15 | TotalBytes int `json:"totalBytes"` 16 | } 17 | 18 | type FileDownloaded struct { 19 | EventId string `json:"-"` 20 | FileId string `json:"fileId"` 21 | } 22 | 23 | type FileDeleted struct { 24 | EventId string `json:"-"` 25 | FileId string `json:"fileId"` 26 | } 27 | 28 | type FolderRemoved struct { 29 | EventId string `json:"-"` 30 | FolderId string `json:"folderId"` 31 | } 32 | -------------------------------------------------------------------------------- /backend/drive/ports/start_download.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/drive/usecases" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | func (server RPCServer) StartDownload(request *types.StartDownloadRequest, response *types.StartDownloadResponse) (err error) { 11 | fileId, err := uuid.Parse(request.FileId) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | result, err := server.svc.StartDownload.Handle( 17 | context.Background(), 18 | usecases.StartDownloadArgs{ 19 | SessionId: request.SessionId, 20 | FileId: fileId, 21 | }, 22 | ) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | response.FileName = result.FileName 28 | response.FileHash = result.FileHash 29 | 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /backend/channel/domain/member.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | const ( 9 | RoleAdmin = "Admin" 10 | RoleMember = "Member" 11 | ) 12 | 13 | type Member struct { 14 | Id uuid.UUID 15 | ChannelId uuid.UUID 16 | Account string 17 | Role string 18 | JoinAt time.Time 19 | } 20 | 21 | func NewChannelMember(channelId uuid.UUID, account string, role string) (*Member, error) { 22 | return &Member{ 23 | Id: uuid.New(), 24 | ChannelId: channelId, 25 | Account: account, 26 | Role: role, 27 | JoinAt: time.Now(), 28 | }, nil 29 | } 30 | 31 | func (m *Member) CanReadMessage() bool { 32 | return true 33 | } 34 | 35 | func (m *Member) CanSendMessage() bool { 36 | return true 37 | } 38 | 39 | func (m *Member) CanSendFile() bool { 40 | return m.Role == RoleAdmin 41 | } 42 | -------------------------------------------------------------------------------- /backend/pkg/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Error struct { 8 | code string // Classification of error 9 | message string // Detailed information about error 10 | err error // Optional original error 11 | } 12 | 13 | func New(code string, message string, err error) *Error { 14 | return &Error{ 15 | code: code, 16 | message: message, 17 | err: err, 18 | } 19 | } 20 | 21 | func (e *Error) Error() string { 22 | msg := fmt.Sprintf("%s: %s", e.code, e.message) 23 | 24 | if e.err != nil { 25 | msg = fmt.Sprintf("%s\ncaused by: %s", msg, e.err.Error()) 26 | } 27 | 28 | return msg 29 | } 30 | 31 | func (e *Error) Unwrap() error { 32 | return e.err 33 | } 34 | 35 | func (e *Error) Code() string { 36 | return e.code 37 | } 38 | 39 | func (e *Error) Message() string { 40 | return e.message 41 | } 42 | -------------------------------------------------------------------------------- /backend/pkg/types/errors.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | var ( 8 | ErrCodeInternal = "Internal" 9 | ErrCodeUnauthenticated = "Unauthenticated" 10 | ErrCodeUnauthorized = "Unauthorized" 11 | ) 12 | 13 | type ErrorResponse struct { 14 | Code string `json:"code"` 15 | Message string `json:"message"` 16 | Detail string `json:"detail"` 17 | } 18 | 19 | func (response ErrorResponse) Error() string { 20 | data, err := json.Marshal(response) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | return string(data) 26 | } 27 | 28 | func NewErrorResponseFromRPC(err error) (ErrorResponse, error) { 29 | response := ErrorResponse{} 30 | 31 | marshalErr := json.Unmarshal([]byte(err.Error()), &response) 32 | if marshalErr != nil { 33 | return ErrorResponse{}, marshalErr 34 | } 35 | 36 | return response, nil 37 | } 38 | -------------------------------------------------------------------------------- /backend/storage/adapters/capacity_repository_test.go: -------------------------------------------------------------------------------- 1 | package adapters_test 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/storage/adapters" 6 | "os" 7 | "path" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestRedisCapacityRepository(t *testing.T) { 13 | connectString := "redis://localhost:6379" 14 | testDir := path.Join(os.TempDir(), "drive-test") 15 | 16 | repo, err := adapters.NewRedisCapacityRepository(connectString, "https://storage.example.com", testDir, 50) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | ticker := time.NewTicker(20 * time.Millisecond) 22 | deadline := time.After(10 * time.Second) 23 | 24 | for { 25 | select { 26 | case <-ticker.C: 27 | err = repo.DecreaseRequestCapacity(context.Background()) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | case <-deadline: 32 | return 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /backend/drive/ports/get_path.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/drive/domain" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/gofiber/fiber/v2" 8 | ) 9 | 10 | func (server HTTPServer) GetPath(ctx *fiber.Ctx) (err error) { 11 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 12 | if sessionId == "" { 13 | return ctx.SendStatus(fiber.StatusForbidden) 14 | } 15 | 16 | parent, err := domain.CreateParent(ctx.Params("parent")) 17 | if err != nil { 18 | return ctx.SendStatus(fiber.StatusBadRequest) 19 | } 20 | 21 | result, err := server.svc.GetPath.Handle( 22 | ctx.Context(), 23 | usecases.GetPathArgs{ 24 | SessionId: sessionId, 25 | Parent: parent, 26 | }, 27 | ) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | return ctx.JSON(result) 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/css/markdown.scss: -------------------------------------------------------------------------------- 1 | #md { 2 | blockquote { 3 | margin: 8px 0; 4 | padding-left: 8px; 5 | border-left: 4px solid #49b1f5; 6 | background-color: rgba(73, 177, 245, 0.1); 7 | color: #6a737d; 8 | } 9 | 10 | a { 11 | text-decoration: none; 12 | border-bottom: 2px solid #00a1d6; 13 | } 14 | 15 | p, ol, ul { 16 | line-height: 1.7; 17 | } 18 | 19 | h1, h2, h3, h4, h5, h6 { 20 | color: #2c3e50; 21 | font-weight: bold; 22 | line-height: 1.25; 23 | } 24 | 25 | h1 { 26 | font-size: 2.2rem; 27 | } 28 | 29 | h2 { 30 | font-size: 1.65rem; 31 | padding-bottom: 0.3rem; 32 | border-bottom: 1px solid #eaecef; 33 | } 34 | 35 | h3 { 36 | font-size: 1.35rem; 37 | } 38 | 39 | h4 { 40 | font-size: 1.15rem; 41 | } 42 | 43 | h5 { 44 | font-size: 1.05rem; 45 | } 46 | 47 | h6 { 48 | font-size: 1rem; 49 | } 50 | } -------------------------------------------------------------------------------- /backend/user/domain/account.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "errors" 4 | 5 | type Account struct { 6 | value string 7 | } 8 | 9 | func NewAccount(account string) (Account, error) { 10 | if len(account) < 3 || len(account) > 30 { 11 | return Account{}, errors.New("invalid account length") 12 | } 13 | 14 | for i := 0; i < len(account); i++ { 15 | if 'a' <= account[i] && account[i] <= 'z' { 16 | continue 17 | } 18 | if 'A' <= account[i] && account[i] <= 'Z' { 19 | continue 20 | } 21 | if '0' <= account[i] && account[i] <= '9' { 22 | continue 23 | } 24 | if account[i] == '-' || account[i] == '_' { 25 | continue 26 | } 27 | return Account{}, errors.New("invalid account charset") 28 | } 29 | 30 | return Account{value: account}, nil 31 | } 32 | 33 | func (a Account) IsZero() bool { 34 | return a == Account{} 35 | } 36 | 37 | func (a Account) String() string { 38 | return a.value 39 | } 40 | -------------------------------------------------------------------------------- /backend/user/adapters/session_model.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/user/domain" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type SessionModel struct { 9 | Id string `redis:"-"` 10 | Account string `redis:"account"` 11 | Username string `redis:"username"` 12 | } 13 | 14 | func NewSessionModel(session *domain.Session) SessionModel { 15 | return SessionModel{ 16 | Id: session.Id().String(), 17 | Account: session.Account().String(), 18 | Username: session.Username(), 19 | } 20 | } 21 | 22 | func (model SessionModel) Session() (*domain.Session, error) { 23 | sessionId, err := uuid.Parse(model.Id) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | account, err := domain.NewAccount(model.Account) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | return domain.NewSessionFromRepository(sessionId, account, model.Username) 34 | } 35 | -------------------------------------------------------------------------------- /backend/user/ports/register.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/pkg/types" 5 | "github.com/axli-personal/drive/backend/user/domain" 6 | "github.com/axli-personal/drive/backend/user/usecases" 7 | "github.com/gofiber/fiber/v2" 8 | ) 9 | 10 | func (server HTTPServer) Register(ctx *fiber.Ctx) (err error) { 11 | request := types.RegisterRequest{} 12 | 13 | err = ctx.BodyParser(&request) 14 | if err != nil { 15 | return err 16 | } 17 | 18 | account, err := domain.NewAccount(request.Account) 19 | if err != nil { 20 | return err 21 | } 22 | password, err := domain.NewPassword(request.Password) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | _, err = server.svc.Register.Handle( 28 | ctx.Context(), 29 | usecases.RegisterArgs{ 30 | Account: account, 31 | Password: password, 32 | Username: request.Username, 33 | }, 34 | ) 35 | 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /backend/drive/ports/start_upload.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/drive/domain" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | ) 9 | 10 | func (server RPCServer) StartUpload(request *types.StartUploadRequest, response *types.StartUploadResponse) (err error) { 11 | parent, err := domain.CreateParent(request.FileParent) 12 | if err != nil { 13 | return err 14 | } 15 | 16 | result, err := server.svc.StartUpload.Handle( 17 | context.Background(), 18 | usecases.StartUploadArgs{ 19 | SessionId: request.SessionId, 20 | FileParent: parent, 21 | FileName: request.FileName, 22 | FileHash: request.FileHash, 23 | FileSize: request.FileSize, 24 | }, 25 | ) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | response.FileId = result.FileId.String() 31 | 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /backend/storage/adapters/event_repository_test.go: -------------------------------------------------------------------------------- 1 | package adapters_test 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/pkg/events" 6 | "github.com/axli-personal/drive/backend/storage/adapters" 7 | "github.com/google/uuid" 8 | "testing" 9 | ) 10 | 11 | func TestRedisEventRepository(t *testing.T) { 12 | connectString := "redis://localhost:6379" 13 | 14 | repo, err := adapters.NewRedisEventRepository(connectString) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | err = repo.PublishFileUploaded( 20 | context.Background(), 21 | events.FileUploaded{ 22 | Endpoint: "https://endpoint", 23 | FileId: uuid.New().String(), 24 | TotalBytes: 100, 25 | }, 26 | ) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | err = repo.PublishFileDownloaded( 32 | context.Background(), 33 | events.FileDownloaded{ 34 | FileId: uuid.New().String(), 35 | }, 36 | ) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/src/components/dashboard/Update.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 34 | -------------------------------------------------------------------------------- /frontend/src/components/ui/LabelButton.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 25 | 26 | -------------------------------------------------------------------------------- /backend/drive/ports/share_file.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func (server HTTPServer) ShareFile(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.SendStatus(fiber.StatusForbidden) 16 | } 17 | 18 | request := types.ShareFileRequest{} 19 | 20 | err = ctx.ParamsParser(&request) 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | fileId, err := uuid.Parse(request.FileId) 26 | if err != nil { 27 | return ctx.SendStatus(fiber.StatusBadRequest) 28 | } 29 | 30 | _, err = server.svc.ShareFile.Handle( 31 | context.Background(), 32 | usecases.ShareFileArgs{ 33 | SessionId: sessionId, 34 | FileId: fileId, 35 | }, 36 | ) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /backend/drive/usecases/types.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/domain" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type FolderLink struct { 9 | Id uuid.UUID 10 | Name string 11 | } 12 | 13 | type FileLink struct { 14 | Id uuid.UUID 15 | Name string 16 | Bytes int 17 | } 18 | 19 | type Children struct { 20 | Folders []FolderLink 21 | Files []FileLink 22 | } 23 | 24 | func ToChildren(folders []*domain.Folder, files []*domain.File) Children { 25 | folderLinks := make([]FolderLink, len(folders)) 26 | filesLinks := make([]FileLink, len(files)) 27 | 28 | for i := 0; i < len(folders); i++ { 29 | folderLinks[i] = FolderLink{ 30 | Id: folders[i].Id(), 31 | Name: folders[i].Name(), 32 | } 33 | } 34 | for i := 0; i < len(files); i++ { 35 | filesLinks[i] = FileLink{ 36 | Id: files[i].Id(), 37 | Name: files[i].Name(), 38 | Bytes: files[i].Size(), 39 | } 40 | } 41 | 42 | return Children{ 43 | Folders: folderLinks, 44 | Files: filesLinks, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /backend/drive/ports/remove_file.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func (server HTTPServer) RemoveFile(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.SendStatus(fiber.StatusForbidden) 16 | } 17 | 18 | request := types.RemoveFileRequest{} 19 | 20 | err = ctx.ParamsParser(&request) 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | fileId, err := uuid.Parse(request.FileId) 26 | if err != nil { 27 | return ctx.SendStatus(fiber.StatusBadRequest) 28 | } 29 | 30 | _, err = server.svc.RemoveFile.Handle( 31 | context.Background(), 32 | usecases.RemoveFileArgs{ 33 | SessionId: sessionId, 34 | FileId: fileId, 35 | }, 36 | ) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /backend/drive/ports/restore_file.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func (server HTTPServer) RestoreFile(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.SendStatus(fiber.StatusForbidden) 16 | } 17 | 18 | request := types.RestoreFileRequest{} 19 | 20 | err = ctx.ParamsParser(&request) 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | fileId, err := uuid.Parse(request.FileId) 26 | if err != nil { 27 | return ctx.SendStatus(fiber.StatusBadRequest) 28 | } 29 | 30 | _, err = server.svc.RestoreFile.Handle( 31 | context.Background(), 32 | usecases.RestoreFileArgs{ 33 | SessionId: sessionId, 34 | FileId: fileId, 35 | }, 36 | ) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /frontend/public/icon/general/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/common/decorator/handler.go: -------------------------------------------------------------------------------- 1 | package decorator 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | type Handler[A any, R any] interface { 10 | Handle(ctx context.Context, args A) (R, error) 11 | } 12 | 13 | func WithLogging[A any, R any](handler Handler[A, R], logger *logrus.Entry) Handler[A, R] { 14 | return loggingHandler[A, R]{ 15 | base: handler, 16 | logger: logger, 17 | } 18 | } 19 | 20 | type loggingHandler[A any, R any] struct { 21 | base Handler[A, R] 22 | logger *logrus.Entry 23 | } 24 | 25 | func (handler loggingHandler[A, R]) Handle(ctx context.Context, args A) (result R, err error) { 26 | logger := handler.logger.WithFields( 27 | logrus.Fields{ 28 | "Handler": fmt.Sprintf("%T", handler.base), 29 | "Args": fmt.Sprintf("%v", args), 30 | }, 31 | ) 32 | 33 | logger.Debug("Start") 34 | defer func() { 35 | if err == nil { 36 | logger.Info("Success") 37 | } else { 38 | logger.WithError(err).Error("Fail") 39 | } 40 | }() 41 | 42 | return handler.base.Handle(ctx, args) 43 | } 44 | -------------------------------------------------------------------------------- /backend/drive/ports/share_folder.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func (server HTTPServer) ShareFolder(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.SendStatus(fiber.StatusForbidden) 16 | } 17 | 18 | request := types.ShareFolderRequest{} 19 | 20 | err = ctx.ParamsParser(&request) 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | folderId, err := uuid.Parse(request.FolderId) 26 | if err != nil { 27 | return ctx.SendStatus(fiber.StatusBadRequest) 28 | } 29 | 30 | _, err = server.svc.ShareFolder.Handle( 31 | context.Background(), 32 | usecases.ShareFolderArgs{ 33 | SessionId: sessionId, 34 | FolderId: folderId, 35 | }, 36 | ) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /backend/drive/ports/remove_folder.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func (server HTTPServer) RemoveFolder(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.SendStatus(fiber.StatusForbidden) 16 | } 17 | 18 | request := types.RemoveFolderRequest{} 19 | 20 | err = ctx.ParamsParser(&request) 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | folderId, err := uuid.Parse(request.FolderId) 26 | if err != nil { 27 | return ctx.SendStatus(fiber.StatusBadRequest) 28 | } 29 | 30 | _, err = server.svc.RemoveFolder.Handle( 31 | context.Background(), 32 | usecases.RemoveFolderArgs{ 33 | SessionId: sessionId, 34 | FolderId: folderId, 35 | }, 36 | ) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /backend/drive/domain/state.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrInvalidState = errors.New("invalid state") 7 | ) 8 | 9 | var ( 10 | StateLocked = State{"Locked"} 11 | StatePrivate = State{"Private"} 12 | StateShared = State{"Shared"} 13 | StateTrashedRoot = State{"TrashedRoot"} 14 | StateTrashed = State{"Trashed"} 15 | ) 16 | 17 | type State struct { 18 | value string 19 | } 20 | 21 | func CreateState(value string) (State, error) { 22 | if value == StateLocked.value { 23 | return StateLocked, nil 24 | } 25 | if value == StatePrivate.value { 26 | return StatePrivate, nil 27 | } 28 | if value == StateShared.value { 29 | return StateShared, nil 30 | } 31 | if value == StateTrashedRoot.value { 32 | return StateTrashedRoot, nil 33 | } 34 | if value == StateTrashed.value { 35 | return StateTrashed, nil 36 | } 37 | 38 | return State{}, ErrInvalidState 39 | } 40 | 41 | func (state State) IsZero() bool { 42 | return state == State{} 43 | } 44 | 45 | func (state State) Value() string { 46 | return state.value 47 | } 48 | -------------------------------------------------------------------------------- /backend/drive/ports/restore_folder.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func (server HTTPServer) RestoreFolder(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.SendStatus(fiber.StatusForbidden) 16 | } 17 | 18 | request := types.RestoreFolderRequest{} 19 | 20 | err = ctx.ParamsParser(&request) 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | folderId, err := uuid.Parse(request.FolderId) 26 | if err != nil { 27 | return ctx.SendStatus(fiber.StatusBadRequest) 28 | } 29 | 30 | _, err = server.svc.RestoreFolder.Handle( 31 | context.Background(), 32 | usecases.RestoreFolderArgs{ 33 | SessionId: sessionId, 34 | FolderId: folderId, 35 | }, 36 | ) 37 | 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /backend/user/adapters/user_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/user/domain" 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | type UserRepository struct { 11 | db *gorm.DB 12 | } 13 | 14 | func NewUserRepository(connectionString string) (UserRepository, error) { 15 | db, err := gorm.Open(mysql.Open(connectionString)) 16 | if err != nil { 17 | return UserRepository{}, err 18 | } 19 | 20 | err = db.AutoMigrate(&UserModel{}) 21 | if err != nil { 22 | return UserRepository{}, err 23 | } 24 | 25 | return UserRepository{db: db}, nil 26 | } 27 | 28 | func (repo UserRepository) SaveUser(ctx context.Context, user *domain.User) error { 29 | model := NewUserModel(user) 30 | 31 | return repo.db.Create(&model).Error 32 | } 33 | 34 | func (repo UserRepository) GetUser(ctx context.Context, account domain.Account) (*domain.User, error) { 35 | model := UserModel{} 36 | 37 | err := repo.db.Take(&model, "account = ?", account.String()).Error 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | return model.User() 43 | } 44 | -------------------------------------------------------------------------------- /backend/user/adapters/user_model.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/user/domain" 5 | ) 6 | 7 | type UserModel struct { 8 | Account string `gorm:"size:30;primaryKey"` 9 | Username string `gorm:"size:64"` 10 | Password string `gorm:"size:30"` 11 | Introduction string `gorm:"size:256"` 12 | } 13 | 14 | func (model UserModel) TableName() string { 15 | return "users" 16 | } 17 | 18 | func NewUserModel(user *domain.User) UserModel { 19 | return UserModel{ 20 | Account: user.Account().String(), 21 | Username: user.Username(), 22 | Password: user.Password().String(), 23 | Introduction: user.Introduction(), 24 | } 25 | } 26 | 27 | func (model UserModel) User() (*domain.User, error) { 28 | account, err := domain.NewAccount(model.Account) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | password, err := domain.NewPassword(model.Password) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return domain.NewUserFromRepository( 39 | account, 40 | password, 41 | model.Username, 42 | model.Introduction, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /backend/storage/ports/download.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/pkg/types" 6 | "github.com/axli-personal/drive/backend/storage/usecases" 7 | "github.com/gofiber/fiber/v2" 8 | "path/filepath" 9 | ) 10 | 11 | func (server HTTPServer) Download(ctx *fiber.Ctx) (err error) { 12 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 13 | 14 | request := types.GetObjectRequest{} 15 | 16 | err = ctx.ParamsParser(&request) 17 | if err != nil { 18 | return ctx.SendStatus(fiber.StatusBadRequest) 19 | } 20 | 21 | err = ctx.QueryParser(&request) 22 | if err != nil { 23 | return ctx.SendStatus(fiber.StatusBadRequest) 24 | } 25 | 26 | result, err := server.svc.DownloadObject.Handle( 27 | ctx.Context(), 28 | usecases.DownloadArgs{ 29 | SessionId: sessionId, 30 | FileId: request.FileId, 31 | }, 32 | ) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | if request.Download { 38 | ctx.Attachment(result.FileName) 39 | } 40 | 41 | return ctx.Type(filepath.Ext(result.FileName)).SendStream(result.Data) 42 | } 43 | -------------------------------------------------------------------------------- /backend/drive/ports/create_folder.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/drive/domain" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func (server HTTPServer) CreateFolder(ctx *fiber.Ctx) (err error) { 12 | request := types.CreateFolderRequest{} 13 | 14 | err = ctx.BodyParser(&request) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 20 | if sessionId == "" { 21 | return ctx.SendStatus(fiber.StatusForbidden) 22 | } 23 | 24 | parent, err := domain.CreateParent(request.Parent) 25 | if err != nil { 26 | return err 27 | } 28 | 29 | _, err = server.svc.CreateFolder.Handle( 30 | ctx.Context(), 31 | usecases.CreateFolderArgs{ 32 | SessionId: sessionId, 33 | FolderParent: parent, 34 | FolderName: request.FolderName, 35 | }, 36 | ) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return ctx.SendStatus(fiber.StatusOK) 42 | } 43 | -------------------------------------------------------------------------------- /backend/storage/ports/upload.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/storage/usecases" 6 | "github.com/gofiber/fiber/v2" 7 | ) 8 | 9 | func (server HTTPServer) Upload(ctx *fiber.Ctx) (err error) { 10 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 11 | if sessionId == "" { 12 | return ctx.SendStatus(fiber.StatusForbidden) 13 | } 14 | 15 | fileParent := ctx.FormValue("parent") 16 | if fileParent == "" { 17 | return ctx.SendStatus(fiber.StatusBadRequest) 18 | } 19 | 20 | fileHeader, err := ctx.FormFile("file") 21 | if err != nil { 22 | return ctx.SendStatus(fiber.StatusBadRequest) 23 | } 24 | 25 | file, err := fileHeader.Open() 26 | if err != nil { 27 | return err 28 | } 29 | 30 | _, err = server.svc.UploadObject.Handle( 31 | ctx.Context(), 32 | usecases.UploadArgs{ 33 | SessionId: sessionId, 34 | FileParent: fileParent, 35 | FileName: fileHeader.Filename, 36 | Data: file, 37 | }, 38 | ) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return ctx.SendStatus(fiber.StatusOK) 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/components/dashboard/Security.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 37 | -------------------------------------------------------------------------------- /backend/storage/domain/object.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "errors" 7 | "io" 8 | ) 9 | 10 | var ( 11 | ErrInvalidData = errors.New("invalid data") 12 | ) 13 | 14 | type Object struct { 15 | hash string 16 | totalBytes int 17 | data io.Reader 18 | } 19 | 20 | func (object *Object) Hash() string { 21 | return object.hash 22 | } 23 | 24 | func (object *Object) Read(p []byte) (int, error) { 25 | return object.data.Read(p) 26 | } 27 | 28 | func (object *Object) TotalBytes() int { 29 | return object.totalBytes 30 | } 31 | 32 | func NewObject(data io.ReadSeeker) (*Object, error) { 33 | if data == nil { 34 | return nil, ErrInvalidData 35 | } 36 | 37 | sha1Hash := sha1.New() 38 | 39 | totalBytes, err := io.Copy(sha1Hash, data) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | _, err = data.Seek(0, io.SeekStart) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | hash := base64.RawURLEncoding.EncodeToString(sha1Hash.Sum(nil)) 50 | 51 | return &Object{ 52 | hash: hash, 53 | totalBytes: int(totalBytes), 54 | data: data, 55 | }, nil 56 | } 57 | -------------------------------------------------------------------------------- /backend/storage/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/storage/ports" 5 | "github.com/axli-personal/drive/backend/storage/service" 6 | "github.com/caarlos0/env/v7" 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/gofiber/fiber/v2/middleware/cors" 9 | "sync" 10 | ) 11 | 12 | func main() { 13 | config := service.Config{} 14 | 15 | err := env.Parse(&config) 16 | if err != nil { 17 | panic(err) 18 | } 19 | 20 | svc, err := service.NewService(config) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | waitGroup := sync.WaitGroup{} 26 | 27 | waitGroup.Add(1) 28 | go func() { 29 | defer waitGroup.Done() 30 | 31 | httpServer := ports.NewHTTPServer(svc) 32 | 33 | app := fiber.New( 34 | fiber.Config{ 35 | BodyLimit: 2 * 1000 * 1024 * 1024, 36 | }, 37 | ) 38 | 39 | app.Use(cors.New(cors.Config{ 40 | AllowCredentials: true, 41 | })) 42 | 43 | app.Post("/upload", httpServer.Upload) 44 | app.Get("/download/:fileId", httpServer.Download) 45 | 46 | err := app.Listen(":8080") 47 | if err != nil { 48 | panic(err) 49 | } 50 | }() 51 | 52 | waitGroup.Wait() 53 | } 54 | -------------------------------------------------------------------------------- /backend/channel/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/google/uuid" 7 | ) 8 | 9 | type Page struct { 10 | From int 11 | Limit int 12 | } 13 | 14 | type ChannelRepository interface { 15 | SaveChannel(ctx context.Context, channel *domain.Channel) error 16 | GetChannel(ctx context.Context, channelId uuid.UUID) (*domain.Channel, error) 17 | FindChannel(ctx context.Context, account string) ([]*domain.Channel, error) 18 | } 19 | 20 | type MemberRepository interface { 21 | SaveMember(ctx context.Context, user *domain.Member) error 22 | FindMember(ctx context.Context, channelId uuid.UUID, account string) (*domain.Member, error) 23 | } 24 | 25 | type MessageRepository interface { 26 | SaveMessage(ctx context.Context, message *domain.Message) error 27 | FindMessage(ctx context.Context, channelId uuid.UUID, page Page) ([]*domain.Message, error) 28 | } 29 | 30 | type FileRepository interface { 31 | SaveFile(ctx context.Context, file *domain.File) error 32 | FindFile(ctx context.Context, channelId uuid.UUID, page Page) ([]*domain.File, error) 33 | } 34 | -------------------------------------------------------------------------------- /backend/drive/domain/folder.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "time" 6 | ) 7 | 8 | type Folder struct { 9 | id uuid.UUID 10 | Metadata 11 | } 12 | 13 | func NewFolder(driveId uuid.UUID, parent Parent, name string) (*Folder, error) { 14 | if driveId == uuid.Nil { 15 | return nil, ErrInvalidUUID 16 | } 17 | if parent.IsZero() { 18 | return nil, ErrInvalidParent 19 | } 20 | 21 | return &Folder{ 22 | id: uuid.New(), 23 | Metadata: Metadata{ 24 | driveId: driveId, 25 | parent: parent, 26 | name: name, 27 | state: StatePrivate, 28 | lastChange: time.Now(), 29 | }, 30 | }, nil 31 | } 32 | 33 | func NewFolderFromRepository( 34 | id uuid.UUID, 35 | driveId uuid.UUID, 36 | parent Parent, 37 | name string, 38 | state State, 39 | lastChange time.Time, 40 | ) (*Folder, error) { 41 | return &Folder{ 42 | id: id, 43 | Metadata: Metadata{ 44 | driveId: driveId, 45 | parent: parent, 46 | name: name, 47 | state: state, 48 | lastChange: lastChange, 49 | }, 50 | }, nil 51 | } 52 | 53 | func (folder *Folder) Id() uuid.UUID { 54 | return folder.id 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/components/TopNav.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | 18 | 60 | -------------------------------------------------------------------------------- /backend/user/ports/login.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/pkg/types" 6 | "github.com/axli-personal/drive/backend/user/domain" 7 | "github.com/axli-personal/drive/backend/user/usecases" 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func (server HTTPServer) Login(ctx *fiber.Ctx) (err error) { 12 | request := types.LoginRequest{} 13 | 14 | err = ctx.BodyParser(&request) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | account, err := domain.NewAccount(request.Account) 20 | if err != nil { 21 | return err 22 | } 23 | password, err := domain.NewPassword(request.Password) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | result, err := server.svc.Login.Handle( 29 | ctx.Context(), 30 | usecases.LoginArgs{ 31 | Account: account, 32 | Password: password, 33 | }, 34 | ) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | ctx.Cookie(&fiber.Cookie{ 40 | Name: auth.SessionIdCookieKey, 41 | Value: result.SessionId.String(), 42 | SameSite: fiber.CookieSameSiteNoneMode, 43 | Secure: true, 44 | }) 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /backend/drive/adapters/repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/repository" 5 | "gorm.io/driver/mysql" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type GormRepository struct { 10 | db *gorm.DB 11 | } 12 | 13 | func NewMysqlRepository(dsn string) (repository.Repository, error) { 14 | db, err := gorm.Open(mysql.Open(dsn)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | 19 | err = db.AutoMigrate(&DriveModel{}, &FolderModel{}, &FileModel{}) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return GormRepository{db: db}, nil 25 | } 26 | 27 | func (repo GormRepository) Transaction(fn func(repo repository.Repository) error) error { 28 | return repo.db.Transaction(func(tx *gorm.DB) error { 29 | return fn(GormRepository{db: tx}) 30 | }) 31 | } 32 | 33 | func (repo GormRepository) GetDriveRepo() repository.DriveRepository { 34 | return GormDriveRepository{db: repo.db} 35 | } 36 | 37 | func (repo GormRepository) GetFolderRepo() repository.FolderRepository { 38 | return GormFolderRepository{db: repo.db} 39 | } 40 | 41 | func (repo GormRepository) GetFileRepo() repository.FileRepository { 42 | return GormFileRepository{db: repo.db} 43 | } 44 | -------------------------------------------------------------------------------- /frontend/src/views/identity/Login.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 44 | -------------------------------------------------------------------------------- /backend/drive/adapters/drive_model.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/domain" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | type DriveModel struct { 9 | Id string `gorm:"size:36;primaryKey"` 10 | Owner string 11 | UsedBytes int 12 | PlanName string 13 | MaxBytes int 14 | } 15 | 16 | func NewDriveModel(drive *domain.Drive) DriveModel { 17 | return DriveModel{ 18 | Id: drive.Id().String(), 19 | Owner: drive.Owner(), 20 | UsedBytes: drive.Usage().Bytes(), 21 | PlanName: drive.Plan().Name(), 22 | MaxBytes: drive.Plan().MaxBytes(), 23 | } 24 | } 25 | 26 | func (model DriveModel) TableName() string { 27 | return "drives" 28 | } 29 | 30 | func (model DriveModel) Drive() (*domain.Drive, error) { 31 | id, err := uuid.Parse(model.Id) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | usage, err := domain.NewStorageUsage(model.UsedBytes) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | plan, err := domain.NewStoragePlan(model.PlanName, model.MaxBytes) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return domain.NewDriveFromRepository( 47 | id, 48 | model.Owner, 49 | usage, 50 | plan, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /backend/user/usecases/register.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/user/domain" 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type RegisterArgs struct { 11 | Account domain.Account 12 | Password domain.Password 13 | Username string 14 | } 15 | 16 | type RegisterResult struct { 17 | } 18 | 19 | type registerHandler struct { 20 | userRepo domain.UserRepository 21 | } 22 | 23 | func (handler registerHandler) Handle(ctx context.Context, args RegisterArgs) (result RegisterResult, err error) { 24 | user, err := domain.NewUser(args.Account, args.Password, args.Username) 25 | if err != nil { 26 | return RegisterResult{}, err 27 | } 28 | 29 | err = handler.userRepo.SaveUser(ctx, user) 30 | if err != nil { 31 | return RegisterResult{}, err 32 | } 33 | 34 | return RegisterResult{}, nil 35 | } 36 | 37 | type RegisterHandler decorator.Handler[RegisterArgs, RegisterResult] 38 | 39 | func NewRegisterHandler( 40 | userRepo domain.UserRepository, 41 | logger *logrus.Entry, 42 | ) RegisterHandler { 43 | return decorator.WithLogging[RegisterArgs, RegisterResult]( 44 | registerHandler{ 45 | userRepo: userRepo, 46 | }, 47 | logger, 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /backend/drive/adapters/user_service.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | stderr "errors" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/axli-personal/drive/backend/pkg/types" 10 | "net/rpc" 11 | ) 12 | 13 | type RPCUserService struct { 14 | client *rpc.Client 15 | } 16 | 17 | func NewRPCUserService(address string) (remote.UserService, error) { 18 | client, err := rpc.DialHTTP("tcp", address) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return RPCUserService{client: client}, nil 24 | } 25 | 26 | func (service RPCUserService) GetUser(ctx context.Context, sessionId string) (domain.User, error) { 27 | request := types.GetUserRequest{SessionId: sessionId} 28 | response := types.GetUserResponse{} 29 | 30 | err := service.client.Call("RPCServer.GetUser", &request, &response) 31 | if err != nil { 32 | if errResponse, err := types.NewErrorResponseFromRPC(err); err == nil { 33 | return domain.User{}, errors.New(errResponse.Code, errResponse.Message, stderr.New(errResponse.Detail)) 34 | } 35 | return domain.User{}, err 36 | } 37 | 38 | return domain.NewUserFromService(response.Account), nil 39 | } 40 | -------------------------------------------------------------------------------- /backend/user/ports/get_user.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/pkg/errors" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/axli-personal/drive/backend/user/usecases" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | func (server RPCServer) GetUser(request *types.GetUserRequest, response *types.GetUserResponse) (err error) { 12 | sessionId, err := uuid.Parse(request.SessionId) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | result, err := server.svc.GetUser.Handle( 18 | context.Background(), 19 | usecases.GetUserArgs{ 20 | SessionId: sessionId, 21 | }, 22 | ) 23 | if err != nil { 24 | if err, ok := err.(*errors.Error); ok { 25 | if err.Code() == usecases.ErrCodeNotLogin { 26 | return types.ErrorResponse{ 27 | Code: types.ErrCodeUnauthenticated, 28 | Message: "authentication failed", 29 | Detail: err.Error(), 30 | } 31 | } 32 | } 33 | return types.ErrorResponse{ 34 | Code: types.ErrCodeInternal, 35 | Message: "fail to get user", 36 | Detail: err.Error(), 37 | } 38 | } 39 | 40 | response.Account = result.Account.String() 41 | response.Username = result.Username 42 | response.Introduction = result.Introduction 43 | 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /backend/storage/adapters/drive_service.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/pkg/types" 5 | "github.com/axli-personal/drive/backend/storage/remote" 6 | "net/rpc" 7 | ) 8 | 9 | type RPCDriveService struct { 10 | client *rpc.Client 11 | } 12 | 13 | func NewRPCDriveService(address string) (remote.DriveService, error) { 14 | client, err := rpc.DialHTTP("tcp", address) 15 | 16 | return RPCDriveService{client: client}, err 17 | } 18 | 19 | func (service RPCDriveService) StartUpload(request types.StartUploadRequest) (types.StartUploadResponse, error) { 20 | response := types.StartUploadResponse{} 21 | 22 | err := service.client.Call("RPCServer.StartUpload", &request, &response) 23 | 24 | return response, err 25 | } 26 | 27 | func (service RPCDriveService) StartDownload(request types.StartDownloadRequest) (types.StartDownloadResponse, error) { 28 | response := types.StartDownloadResponse{} 29 | 30 | err := service.client.Call("RPCServer.StartDownload", &request, &response) 31 | 32 | return response, err 33 | } 34 | 35 | func (service RPCDriveService) FinishUpload(request types.FinishUploadRequest) (types.FinishUploadResponse, error) { 36 | response := types.FinishUploadResponse{} 37 | 38 | err := service.client.Call("RPCServer.FinishUpload", &request, &response) 39 | 40 | return response, err 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/components/ui/LabelInput.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 41 | 42 | -------------------------------------------------------------------------------- /backend/drive/ports/get_file.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/drive/usecases" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | func (server HTTPServer) GetFile(ctx *fiber.Ctx) (err error) { 12 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 13 | 14 | request := types.GetFileRequest{} 15 | 16 | err = ctx.ParamsParser(&request) 17 | if err != nil { 18 | return ctx.SendStatus(fiber.StatusBadRequest) 19 | } 20 | 21 | fileId, err := uuid.Parse(request.FileId) 22 | if err != nil || fileId == uuid.Nil { 23 | return ctx.SendStatus(fiber.StatusBadRequest) 24 | } 25 | 26 | result, err := server.svc.GetFile.Handle( 27 | ctx.Context(), 28 | usecases.GetFileArgs{ 29 | SessionId: sessionId, 30 | FileId: fileId, 31 | }, 32 | ) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | response := types.GetFileResponse{ 38 | FileId: result.FileId.String(), 39 | Parent: result.Parent.String(), 40 | Name: result.Name, 41 | Shared: result.Shared, 42 | LastChange: result.LastChange, 43 | Bytes: result.Bytes, 44 | DownloadCounts: result.DownloadCounts, 45 | } 46 | 47 | return ctx.JSON(response) 48 | } 49 | -------------------------------------------------------------------------------- /backend/user/domain/user.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | ErrMissingAccount = errors.New("missing account") 9 | ErrMissingPassword = errors.New("missing password") 10 | ) 11 | 12 | type User struct { 13 | account Account 14 | password Password 15 | username string 16 | introduction string 17 | } 18 | 19 | func NewUser(account Account, password Password, username string) (*User, error) { 20 | if account.IsZero() { 21 | return nil, ErrMissingAccount 22 | } 23 | if password.IsZero() { 24 | return nil, ErrMissingPassword 25 | } 26 | 27 | return &User{ 28 | account: account, 29 | password: password, 30 | username: username, 31 | }, nil 32 | } 33 | 34 | func NewUserFromRepository( 35 | account Account, 36 | password Password, 37 | username string, 38 | introduction string, 39 | ) (*User, error) { 40 | return &User{ 41 | account: account, 42 | username: username, 43 | password: password, 44 | introduction: introduction, 45 | }, nil 46 | } 47 | 48 | func (user *User) Account() Account { 49 | return user.account 50 | } 51 | 52 | func (user *User) Password() Password { 53 | return user.password 54 | } 55 | 56 | func (user *User) Username() string { 57 | return user.username 58 | } 59 | 60 | func (user *User) Introduction() string { 61 | return user.introduction 62 | } 63 | -------------------------------------------------------------------------------- /backend/storage/adapters/object_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/storage/domain" 6 | "github.com/axli-personal/drive/backend/storage/repository" 7 | "io" 8 | "os" 9 | "path" 10 | ) 11 | 12 | type DiskObjectRepository struct { 13 | dirPath string 14 | } 15 | 16 | func NewDiskObjectRepository(directoryPath string) (repository.ObjectRepository, error) { 17 | err := os.MkdirAll(directoryPath, 0700) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | return DiskObjectRepository{ 23 | dirPath: directoryPath, 24 | }, nil 25 | } 26 | 27 | func (repo DiskObjectRepository) SaveObject(ctx context.Context, object *domain.Object) error { 28 | objectPath := path.Join(repo.dirPath, object.Hash()) 29 | 30 | _, err := os.Stat(objectPath) 31 | if os.IsExist(err) { 32 | return nil 33 | } 34 | 35 | file, err := os.Create(objectPath) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | defer file.Close() 41 | 42 | _, err = io.Copy(file, object) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (repo DiskObjectRepository) GetObject(ctx context.Context, hash string) (*domain.Object, error) { 51 | file, err := os.Open(path.Join(repo.dirPath, hash)) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return domain.NewObject(file) 57 | } 58 | -------------------------------------------------------------------------------- /backend/user/domain/session.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | var ( 9 | ErrMissingUsername = errors.New("missing username") 10 | ErrMissingId = errors.New("missing id") 11 | ) 12 | 13 | type Session struct { 14 | id uuid.UUID 15 | account Account 16 | username string 17 | } 18 | 19 | func NewSession(account Account, username string) (*Session, error) { 20 | if account.IsZero() { 21 | return nil, ErrMissingAccount 22 | } 23 | if username == "" { 24 | return nil, ErrMissingUsername 25 | } 26 | 27 | return &Session{ 28 | id: uuid.New(), 29 | account: account, 30 | username: username, 31 | }, nil 32 | } 33 | 34 | func NewSessionFromRepository(id uuid.UUID, account Account, username string) (*Session, error) { 35 | if id == uuid.Nil { 36 | return nil, ErrMissingId 37 | } 38 | if account.IsZero() { 39 | return nil, ErrMissingAccount 40 | } 41 | if username == "" { 42 | return nil, ErrMissingUsername 43 | } 44 | 45 | return &Session{ 46 | id: id, 47 | account: account, 48 | username: username, 49 | }, nil 50 | } 51 | 52 | func (session *Session) Id() uuid.UUID { 53 | return session.id 54 | } 55 | 56 | func (session *Session) Account() Account { 57 | return session.account 58 | } 59 | 60 | func (session *Session) Username() string { 61 | return session.username 62 | } 63 | -------------------------------------------------------------------------------- /backend/common/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 5 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 6 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 7 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 8 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 9 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 10 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 11 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ= 12 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 14 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 15 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 云端硬盘 2 | 3 | 提供Google Drive的基本功能, 并做一些扩展. 4 | 5 | ## 功能 6 | 7 | * 文件和目录管理 8 | * 分享 9 | * 回收站 10 | * 容量限制 11 | * 搜索(TODO) 12 | * 共享频道(TODO) 13 | * 管理员接口(TODO) 14 | 15 | ## 部署 16 | 17 | 服务部署: Docker Compose. 18 | 19 | ```shell 20 | $ ./build.sh # 构建脚本 21 | ``` 22 | 23 | ## 服务 24 | 25 | | 服务 | 功能 | 26 | |---------|-----------| 27 | | User | 用户管理 | 28 | | Drive | 命名空间、容量管理 | 29 | | Channel | 发送信息、发送文件 | 30 | | Storage | 存储节点 | 31 | | Mysql | 数据库 | 32 | | Redis | 缓存、消息队列 | 33 | | Web | 客户端 | 34 | 35 | ## 项目结构 36 | 37 | ```text 38 | /backend: 39 | /common: 40 | /auth: authentication 41 | /decorator: handler decorators 42 | /pkg: 43 | /events: shared event definition 44 | /types: shared request and response definition 45 | /errors: error definition 46 | /service: 47 | /adapters: interface implementation 48 | /domain: domain object 49 | /main: main function 50 | /ports: http and rpc handler 51 | /remote: remote service interface 52 | /repository: repository interface 53 | /service: application service 54 | /usecases: use case and event handler 55 | /frontend: web client 56 | /test: performance test 57 | ``` 58 | 59 | ## 数据一致性 60 | 61 | 最终一致性: 保证消息不丢失, 至少被消费一次. 62 | 63 | 兜底措施: 增加定时任务(TODO). 64 | 65 | ## 可用性 66 | 67 | 云存储(TODO), 服务限流(TODO), 自动重启. 68 | 69 | ## 扩展性 70 | 71 | 云存储(TODO). 72 | 73 | ## 性能 74 | 75 | 压力测试, 索引(TODO), 数据库缓存(TODO), 文件缓存(TODO). 76 | -------------------------------------------------------------------------------- /backend/user/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service_test 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/utils" 6 | "github.com/axli-personal/drive/backend/user/domain" 7 | "github.com/axli-personal/drive/backend/user/service" 8 | "github.com/axli-personal/drive/backend/user/usecases" 9 | "github.com/caarlos0/env/v7" 10 | "testing" 11 | ) 12 | 13 | func TestService(t *testing.T) { 14 | config := service.Config{} 15 | 16 | err := env.Parse(&config) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | 21 | svc, err := service.NewService(config) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | 26 | account, err := domain.NewAccount(utils.RandomString(10)) 27 | password, err := domain.NewPassword(utils.RandomString(10)) 28 | username := utils.RandomString(10) 29 | 30 | _, err = svc.Register.Handle( 31 | context.Background(), 32 | usecases.RegisterArgs{ 33 | Account: account, 34 | Password: password, 35 | Username: username, 36 | }, 37 | ) 38 | if err != nil { 39 | t.Error(err) 40 | } 41 | 42 | loginResult, err := svc.Login.Handle( 43 | context.Background(), 44 | usecases.LoginArgs{ 45 | Account: account, 46 | Password: password, 47 | }, 48 | ) 49 | if err != nil { 50 | t.Error(err) 51 | } 52 | 53 | _, err = svc.GetUser.Handle( 54 | context.Background(), 55 | usecases.GetUserArgs{ 56 | SessionId: loginResult.SessionId, 57 | }, 58 | ) 59 | if err != nil { 60 | t.Error(err) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router" 2 | 3 | const routes = [ 4 | { 5 | path: "/register", 6 | component: () => import("/src/views/identity/Register.vue"), 7 | }, 8 | { 9 | path: "/login", 10 | component: () => import("/src/views/identity/Login.vue"), 11 | }, 12 | { 13 | path: "/drive/plan", 14 | component: () => import("/src/views/drive/Plan.vue") 15 | }, 16 | { 17 | path: "/drive/my-drive", 18 | alias: "/", 19 | component: () => import("/src/views/drive/Drive.vue") 20 | }, 21 | { 22 | path: "/drive/my-recycle-bin", 23 | alias: "/", 24 | component: () => import("/src/views/drive/RecycleBin.vue") 25 | }, 26 | { 27 | path: "/drive/folders/:folderId", 28 | component: () => import("/src/views/drive/Folder.vue") 29 | }, 30 | { 31 | path: "/drive/files/binary/:fileId", 32 | component: () => import("/src/views/drive/BinaryFile.vue"), 33 | }, 34 | { 35 | path: "/drive/files/text/:fileId", 36 | component: () => import("/src/views/drive/TextFile.vue"), 37 | }, 38 | { 39 | path: "/drive/files/markdown/:fileId", 40 | component: () => import("/src/views/drive/MarkdownFile.vue"), 41 | }, 42 | ]; 43 | 44 | // the class name for active router-link is "link-match" and "link-same". 45 | export default createRouter({ 46 | history: createWebHistory(), 47 | linkActiveClass: "link-match", 48 | linkExactActiveClass: "link-same", 49 | routes: routes, 50 | }); 51 | -------------------------------------------------------------------------------- /backend/channel/usecases/list_channel.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/axli-personal/drive/backend/channel/repository" 7 | "github.com/axli-personal/drive/backend/common/decorator" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | const ( 13 | ErrCodeInternal = "Internal" 14 | ErrCodeChannelNotFound = "ChannelNotFound" 15 | ) 16 | 17 | type ( 18 | ListChannelArgs struct { 19 | UserAccount string 20 | } 21 | 22 | ListChannelResult struct { 23 | Channels []*domain.Channel 24 | } 25 | ) 26 | 27 | type listChannelHandler struct { 28 | channelRepo repository.ChannelRepository 29 | } 30 | 31 | func (h listChannelHandler) Handle(ctx context.Context, args ListChannelArgs) (ListChannelResult, error) { 32 | channels, err := h.channelRepo.FindChannel(ctx, args.UserAccount) 33 | if err != nil { 34 | return ListChannelResult{}, errors.New(ErrCodeInternal, "fail to find channel", err) 35 | } 36 | 37 | return ListChannelResult{Channels: channels}, nil 38 | } 39 | 40 | type ListChannelHandler decorator.Handler[ListChannelArgs, ListChannelResult] 41 | 42 | func NewListChannelHandler( 43 | channelRepo repository.ChannelRepository, 44 | logger *logrus.Entry, 45 | ) ListChannelHandler { 46 | return decorator.WithLogging[ListChannelArgs, ListChannelResult]( 47 | listChannelHandler{ 48 | channelRepo: channelRepo, 49 | }, 50 | logger, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /backend/drive/usecases/finish_upload.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type FinishUploadArgs struct { 13 | FileId uuid.UUID 14 | } 15 | 16 | type FinishUploadResult struct { 17 | } 18 | 19 | type finishUploadHandler struct { 20 | fileRepo repository.FileRepository 21 | } 22 | 23 | func (handler finishUploadHandler) Handle(ctx context.Context, args FinishUploadArgs) (FinishUploadResult, error) { 24 | file, err := handler.fileRepo.GetFile(ctx, args.FileId) 25 | if err != nil { 26 | return FinishUploadResult{}, err 27 | } 28 | 29 | file.SetState(domain.StatePrivate) 30 | 31 | err = handler.fileRepo.UpdateFile( 32 | ctx, 33 | file, 34 | repository.UpdateFileOptions{ 35 | MustInState: domain.StateLocked, 36 | }, 37 | ) 38 | if err != nil { 39 | return FinishUploadResult{}, err 40 | } 41 | 42 | return FinishUploadResult{}, err 43 | } 44 | 45 | type FinishUploadHandler decorator.Handler[FinishUploadArgs, FinishUploadResult] 46 | 47 | func NewFileUploadedHandler( 48 | fileRepo repository.FileRepository, 49 | logger *logrus.Entry, 50 | ) FinishUploadHandler { 51 | return decorator.WithLogging[FinishUploadArgs, FinishUploadResult]( 52 | finishUploadHandler{ 53 | fileRepo: fileRepo, 54 | }, 55 | logger, 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /backend/user/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/user/ports" 5 | "github.com/axli-personal/drive/backend/user/service" 6 | "github.com/caarlos0/env/v7" 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/gofiber/fiber/v2/middleware/cors" 9 | "net" 10 | "net/http" 11 | "net/rpc" 12 | "sync" 13 | ) 14 | 15 | func main() { 16 | config := service.Config{} 17 | 18 | err := env.Parse(&config) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | svc, err := service.NewService(config) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | waitGroup := sync.WaitGroup{} 29 | 30 | waitGroup.Add(1) 31 | go func() { 32 | defer waitGroup.Done() 33 | 34 | httpServer := ports.NewHTTPServer(svc) 35 | 36 | app := fiber.New() 37 | 38 | app.Use(cors.New(cors.Config{ 39 | AllowCredentials: true, 40 | })) 41 | 42 | app.Post("/register", httpServer.Register) 43 | app.Post("/login", httpServer.Login) 44 | 45 | err := app.Listen(":8080") 46 | if err != nil { 47 | panic(err) 48 | } 49 | }() 50 | 51 | waitGroup.Add(1) 52 | go func() { 53 | defer waitGroup.Done() 54 | 55 | rpcServer := ports.NewRPCServer(svc) 56 | 57 | err := rpc.Register(&rpcServer) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | rpc.HandleHTTP() 63 | 64 | listener, err := net.Listen("tcp", ":8081") 65 | if err != nil { 66 | panic(err) 67 | } 68 | 69 | err = http.Serve(listener, nil) 70 | if err != nil { 71 | panic(err) 72 | } 73 | }() 74 | 75 | waitGroup.Wait() 76 | } 77 | -------------------------------------------------------------------------------- /backend/channel/usecases/create_channel.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/axli-personal/drive/backend/channel/repository" 7 | "github.com/axli-personal/drive/backend/common/decorator" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | CreateChannelArgs struct { 14 | UserAccount string 15 | ChannelName string 16 | } 17 | 18 | CreateChannelResult struct { 19 | } 20 | ) 21 | 22 | type createChannelHandler struct { 23 | channelRepo repository.ChannelRepository 24 | } 25 | 26 | func (h createChannelHandler) Handle(ctx context.Context, args CreateChannelArgs) (CreateChannelResult, error) { 27 | channel, err := domain.NewChannel(args.ChannelName, args.UserAccount) 28 | if err != nil { 29 | return CreateChannelResult{}, errors.New(ErrCodeInternal, "fail to create channel", err) 30 | } 31 | 32 | err = h.channelRepo.SaveChannel(ctx, channel) 33 | if err != nil { 34 | return CreateChannelResult{}, errors.New(ErrCodeInternal, "fail to create channel", err) 35 | } 36 | 37 | return CreateChannelResult{}, nil 38 | } 39 | 40 | type CreateChannelHandler decorator.Handler[CreateChannelArgs, CreateChannelResult] 41 | 42 | func NewCreateChannelHandler( 43 | channelRepo repository.ChannelRepository, 44 | logger *logrus.Entry, 45 | ) CreateChannelHandler { 46 | return decorator.WithLogging[CreateChannelArgs, CreateChannelResult]( 47 | createChannelHandler{ 48 | channelRepo: channelRepo, 49 | }, 50 | logger, 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /backend/storage/adapters/event_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/axli-personal/drive/backend/pkg/events" 7 | "github.com/axli-personal/drive/backend/storage/repository" 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type RedisEventRepository struct { 12 | client *redis.Client 13 | } 14 | 15 | func NewRedisEventRepository(connectionString string) (repository.EventRepository, error) { 16 | options, err := redis.ParseURL(connectionString) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | client := redis.NewClient(options) 22 | 23 | err = client.Ping(context.Background()).Err() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return RedisEventRepository{client: client}, nil 29 | } 30 | 31 | func (repo RedisEventRepository) PublishFileUploaded(ctx context.Context, event events.FileUploaded) error { 32 | body, err := json.Marshal(event) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | return repo.client.XAdd( 38 | ctx, 39 | &redis.XAddArgs{ 40 | Stream: events.StreamFileUploaded, 41 | Values: map[string]any{ 42 | events.FieldBody: body, 43 | }, 44 | }, 45 | ).Err() 46 | } 47 | 48 | func (repo RedisEventRepository) PublishFileDownloaded(ctx context.Context, event events.FileDownloaded) error { 49 | body, err := json.Marshal(event) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | return repo.client.XAdd( 55 | ctx, 56 | &redis.XAddArgs{ 57 | Stream: events.StreamFileDownloaded, 58 | Values: map[string]any{ 59 | events.FieldBody: body, 60 | }, 61 | }, 62 | ).Err() 63 | } 64 | -------------------------------------------------------------------------------- /backend/storage/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axli-personal/drive/backend/storage 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/axli-personal/drive/backend/common v0.0.0 7 | github.com/axli-personal/drive/backend/pkg v0.0.0 8 | github.com/caarlos0/env/v7 v7.0.0 9 | github.com/gofiber/fiber/v2 v2.42.0 10 | github.com/google/uuid v1.3.0 11 | github.com/redis/go-redis/v9 v9.0.2 12 | github.com/sirupsen/logrus v1.9.0 13 | ) 14 | 15 | require ( 16 | github.com/aliyun/aliyun-oss-go-sdk v2.2.7+incompatible // indirect 17 | github.com/andybalholm/brotli v1.0.4 // indirect 18 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 19 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 20 | github.com/klauspost/compress v1.15.9 // indirect 21 | github.com/mattn/go-colorable v0.1.13 // indirect 22 | github.com/mattn/go-isatty v0.0.17 // indirect 23 | github.com/mattn/go-runewidth v0.0.14 // indirect 24 | github.com/philhofer/fwd v1.1.1 // indirect 25 | github.com/rivo/uniseg v0.2.0 // indirect 26 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 27 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect 28 | github.com/tinylib/msgp v1.1.6 // indirect 29 | github.com/valyala/bytebufferpool v1.0.0 // indirect 30 | github.com/valyala/fasthttp v1.44.0 // indirect 31 | github.com/valyala/tcplisten v1.0.0 // indirect 32 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 33 | golang.org/x/time v0.3.0 // indirect 34 | ) 35 | 36 | replace ( 37 | github.com/axli-personal/drive/backend/common => ../common/ 38 | github.com/axli-personal/drive/backend/pkg => ../pkg/ 39 | ) 40 | -------------------------------------------------------------------------------- /frontend/src/components/ui/SlotInput.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 28 | 29 | -------------------------------------------------------------------------------- /backend/drive/adapters/folder_model.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/domain" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | type FolderModel struct { 10 | Id string `gorm:"size:36;primaryKey"` 11 | DriveId string `gorm:"size:36;index:idx_drive_parent_name,unique,priority:1"` 12 | Parent string `gorm:"size:36;index:idx_drive_parent_name,unique,priority:2"` 13 | Name string `gorm:"size:128;index:idx_drive_parent_name,unique,priority:3"` 14 | State string 15 | LastChange time.Time 16 | } 17 | 18 | func NewFolderModel(folder *domain.Folder) FolderModel { 19 | return FolderModel{ 20 | Id: folder.Id().String(), 21 | DriveId: folder.DriveId().String(), 22 | Parent: folder.Parent().String(), 23 | Name: folder.Name(), 24 | State: folder.State().Value(), 25 | LastChange: folder.LastChange(), 26 | } 27 | } 28 | 29 | func (model FolderModel) TableName() string { 30 | return "folders" 31 | } 32 | 33 | func (model FolderModel) Folder() (*domain.Folder, error) { 34 | id, err := uuid.Parse(model.Id) 35 | if err != nil { 36 | return nil, err 37 | } 38 | 39 | driveId, err := uuid.Parse(model.DriveId) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | parent, err := domain.CreateParent(model.Parent) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | state, err := domain.CreateState(model.State) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | return domain.NewFolderFromRepository( 55 | id, 56 | driveId, 57 | parent, 58 | model.Name, 59 | state, 60 | model.LastChange, 61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /frontend/src/views/identity/Register.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 55 | -------------------------------------------------------------------------------- /backend/drive/ports/get_folder.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/drive/usecases" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/google/uuid" 9 | ) 10 | 11 | func (server HTTPServer) GetFolder(ctx *fiber.Ctx) (err error) { 12 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 13 | 14 | request := types.GetFolderRequest{} 15 | 16 | err = ctx.ParamsParser(&request) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | folderId, err := uuid.Parse(request.FolderId) 22 | if err != nil || folderId == uuid.Nil { 23 | return err 24 | } 25 | 26 | result, err := server.svc.GetFolder.Handle( 27 | ctx.Context(), 28 | usecases.GetFolderArgs{ 29 | SessionId: sessionId, 30 | FolderId: folderId, 31 | }, 32 | ) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | response := types.GetFolderResponse{ 38 | FolderId: result.FolderId.String(), 39 | Parent: result.Parent.String(), 40 | Name: result.Name, 41 | Shared: result.Shared, 42 | LastChange: result.LastChange, 43 | Children: types.Children{}, 44 | } 45 | 46 | for _, link := range result.Children.Folders { 47 | response.Children.Folders = append(response.Children.Folders, types.FolderLink{ 48 | Id: link.Id.String(), 49 | Name: link.Name, 50 | }) 51 | } 52 | 53 | for _, link := range result.Children.Files { 54 | response.Children.Files = append(response.Children.Files, types.FileLink{ 55 | Id: link.Id.String(), 56 | Name: link.Name, 57 | Bytes: link.Bytes, 58 | }) 59 | } 60 | 61 | return ctx.JSON(response) 62 | } 63 | -------------------------------------------------------------------------------- /backend/drive/usecases/create_drive.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | CreateDriveArgs struct { 14 | SessionId string 15 | } 16 | 17 | CreateDriveResult struct { 18 | } 19 | ) 20 | 21 | type createDriveHandler struct { 22 | userService remote.UserService 23 | driveRepo repository.DriveRepository 24 | } 25 | 26 | func (handler createDriveHandler) Handle(ctx context.Context, args CreateDriveArgs) (CreateDriveResult, error) { 27 | user, err := handler.userService.GetUser(ctx, args.SessionId) 28 | if err != nil { 29 | return CreateDriveResult{}, err 30 | } 31 | 32 | drive, err := domain.NewDrive(user.Account()) 33 | if err != nil { 34 | return CreateDriveResult{}, err 35 | } 36 | 37 | err = handler.driveRepo.CreateDrive( 38 | ctx, 39 | drive, 40 | repository.CreateDriveOptions{ 41 | OnlyOneDrive: true, 42 | }, 43 | ) 44 | if err != nil { 45 | return CreateDriveResult{}, err 46 | } 47 | 48 | return CreateDriveResult{}, nil 49 | } 50 | 51 | type CreateDriveHandler decorator.Handler[CreateDriveArgs, CreateDriveResult] 52 | 53 | func NewCreateDriveHandler( 54 | userService remote.UserService, 55 | driveRepo repository.DriveRepository, 56 | logger *logrus.Entry, 57 | ) CreateDriveHandler { 58 | return decorator.WithLogging[CreateDriveArgs, CreateDriveResult]( 59 | createDriveHandler{ 60 | userService: userService, 61 | driveRepo: driveRepo, 62 | }, 63 | logger, 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /backend/user/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axli-personal/drive/backend/user 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/axli-personal/drive/backend/common v0.0.0 7 | github.com/axli-personal/drive/backend/pkg v0.0.0 8 | github.com/caarlos0/env/v7 v7.0.0 9 | github.com/gofiber/fiber/v2 v2.42.0 10 | github.com/google/uuid v1.3.0 11 | github.com/redis/go-redis/v9 v9.0.2 12 | github.com/sirupsen/logrus v1.9.0 13 | gorm.io/driver/mysql v1.4.7 14 | gorm.io/gorm v1.24.5 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.4 // indirect 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 21 | github.com/go-sql-driver/mysql v1.7.0 // indirect 22 | github.com/jinzhu/inflection v1.0.0 // indirect 23 | github.com/jinzhu/now v1.1.5 // indirect 24 | github.com/klauspost/compress v1.15.9 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.17 // indirect 27 | github.com/mattn/go-runewidth v0.0.14 // indirect 28 | github.com/philhofer/fwd v1.1.1 // indirect 29 | github.com/rivo/uniseg v0.2.0 // indirect 30 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 31 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect 32 | github.com/tinylib/msgp v1.1.6 // indirect 33 | github.com/valyala/bytebufferpool v1.0.0 // indirect 34 | github.com/valyala/fasthttp v1.44.0 // indirect 35 | github.com/valyala/tcplisten v1.0.0 // indirect 36 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 37 | ) 38 | 39 | replace ( 40 | github.com/axli-personal/drive/backend/common => ../common/ 41 | github.com/axli-personal/drive/backend/pkg => ../pkg/ 42 | ) 43 | -------------------------------------------------------------------------------- /backend/drive/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/axli-personal/drive/backend/drive 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/axli-personal/drive/backend/common v0.0.0 7 | github.com/axli-personal/drive/backend/pkg v0.0.0 8 | github.com/caarlos0/env/v7 v7.0.0 9 | github.com/gofiber/fiber/v2 v2.42.0 10 | github.com/google/uuid v1.3.0 11 | github.com/redis/go-redis/v9 v9.0.2 12 | github.com/sirupsen/logrus v1.9.0 13 | gorm.io/driver/mysql v1.4.7 14 | gorm.io/gorm v1.25.0 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/brotli v1.0.4 // indirect 19 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 20 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 21 | github.com/go-sql-driver/mysql v1.7.0 // indirect 22 | github.com/jinzhu/inflection v1.0.0 // indirect 23 | github.com/jinzhu/now v1.1.5 // indirect 24 | github.com/klauspost/compress v1.15.9 // indirect 25 | github.com/mattn/go-colorable v0.1.13 // indirect 26 | github.com/mattn/go-isatty v0.0.17 // indirect 27 | github.com/mattn/go-runewidth v0.0.14 // indirect 28 | github.com/philhofer/fwd v1.1.1 // indirect 29 | github.com/rivo/uniseg v0.2.0 // indirect 30 | github.com/savsgio/dictpool v0.0.0-20221023140959-7bf2e61cea94 // indirect 31 | github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect 32 | github.com/tinylib/msgp v1.1.6 // indirect 33 | github.com/valyala/bytebufferpool v1.0.0 // indirect 34 | github.com/valyala/fasthttp v1.44.0 // indirect 35 | github.com/valyala/tcplisten v1.0.0 // indirect 36 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect 37 | ) 38 | 39 | replace ( 40 | github.com/axli-personal/drive/backend/common => ../common/ 41 | github.com/axli-personal/drive/backend/pkg => ../pkg/ 42 | ) 43 | -------------------------------------------------------------------------------- /backend/storage/usecases/download.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/axli-personal/drive/backend/storage/remote" 8 | "github.com/axli-personal/drive/backend/storage/repository" 9 | "github.com/sirupsen/logrus" 10 | "io" 11 | ) 12 | 13 | type ( 14 | DownloadArgs struct { 15 | SessionId string 16 | FileId string 17 | } 18 | 19 | DownloadResult struct { 20 | Data io.Reader 21 | FileName string 22 | } 23 | ) 24 | 25 | type downloadHandler struct { 26 | driveService remote.DriveService 27 | objectRepo repository.ObjectRepository 28 | } 29 | 30 | func (handler downloadHandler) Handle(ctx context.Context, args DownloadArgs) (DownloadResult, error) { 31 | response, err := handler.driveService.StartDownload( 32 | types.StartDownloadRequest{ 33 | SessionId: args.SessionId, 34 | FileId: args.FileId, 35 | }, 36 | ) 37 | if err != nil { 38 | return DownloadResult{}, err 39 | } 40 | 41 | object, err := handler.objectRepo.GetObject(ctx, response.FileHash) 42 | if err != nil { 43 | return DownloadResult{}, err 44 | } 45 | 46 | return DownloadResult{ 47 | FileName: response.FileName, 48 | Data: object, 49 | }, nil 50 | } 51 | 52 | type DownloadHandler decorator.Handler[DownloadArgs, DownloadResult] 53 | 54 | func NewDownloadHandler( 55 | driveService remote.DriveService, 56 | objectRepo repository.ObjectRepository, 57 | logger *logrus.Entry, 58 | ) DownloadHandler { 59 | return decorator.WithLogging[DownloadArgs, DownloadResult]( 60 | downloadHandler{ 61 | driveService: driveService, 62 | objectRepo: objectRepo, 63 | }, 64 | logger, 65 | ) 66 | } 67 | -------------------------------------------------------------------------------- /backend/user/usecases/login.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/axli-personal/drive/backend/common/decorator" 7 | "github.com/axli-personal/drive/backend/user/domain" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | "time" 11 | ) 12 | 13 | var ( 14 | ErrWrongPassword = errors.New("wrong password") 15 | ) 16 | 17 | type LoginArgs struct { 18 | Account domain.Account 19 | Password domain.Password 20 | } 21 | 22 | type LoginResult struct { 23 | SessionId uuid.UUID 24 | } 25 | 26 | type loginHandler struct { 27 | userRepo domain.UserRepository 28 | sessionRepo domain.SessionRepository 29 | } 30 | 31 | func (handler loginHandler) Handle(ctx context.Context, args LoginArgs) (LoginResult, error) { 32 | user, err := handler.userRepo.GetUser(ctx, args.Account) 33 | if err != nil { 34 | return LoginResult{}, err 35 | } 36 | 37 | if args.Password != user.Password() { 38 | return LoginResult{}, ErrWrongPassword 39 | } 40 | 41 | session, err := domain.NewSession(user.Account(), user.Username()) 42 | if err != nil { 43 | return LoginResult{}, err 44 | } 45 | 46 | err = handler.sessionRepo.SaveSession(ctx, session, 12*time.Hour) 47 | if err != nil { 48 | return LoginResult{}, err 49 | } 50 | 51 | return LoginResult{ 52 | SessionId: session.Id(), 53 | }, nil 54 | } 55 | 56 | type LoginHandler decorator.Handler[LoginArgs, LoginResult] 57 | 58 | func NewLoginHandler( 59 | userRepo domain.UserRepository, 60 | sessionRepo domain.SessionRepository, 61 | logger *logrus.Entry, 62 | ) LoginHandler { 63 | return decorator.WithLogging[LoginArgs, LoginResult]( 64 | loginHandler{ 65 | sessionRepo: sessionRepo, 66 | userRepo: userRepo, 67 | }, 68 | logger, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /backend/user/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/user/adapters" 5 | "github.com/axli-personal/drive/backend/user/usecases" 6 | "github.com/sirupsen/logrus" 7 | "time" 8 | ) 9 | 10 | type Config struct { 11 | MysqlConnectionString string `env:"MYSQL_CONNECTION_STRING" yaml:"mysql-connection-string"` 12 | RedisConnectionString string `env:"REDIS_CONNECTION_STRING" yaml:"redis-connection-string"` 13 | LogLevel string `env:"LOG_LEVEL" yaml:"log-level"` 14 | } 15 | 16 | type Service struct { 17 | Register usecases.RegisterHandler 18 | Login usecases.LoginHandler 19 | GetUser usecases.GetUserHandler 20 | } 21 | 22 | func NewService(config Config) (Service, error) { 23 | logger, err := NewLogger(config.LogLevel) 24 | if err != nil { 25 | return Service{}, err 26 | } 27 | 28 | userRepo, err := adapters.NewUserRepository(config.MysqlConnectionString) 29 | if err != nil { 30 | return Service{}, err 31 | } 32 | 33 | sessionRepo, err := adapters.NewSessionRepository(config.RedisConnectionString, logger) 34 | if err != nil { 35 | return Service{}, err 36 | } 37 | 38 | return Service{ 39 | Register: usecases.NewRegisterHandler(userRepo, logger), 40 | Login: usecases.NewLoginHandler(userRepo, sessionRepo, logger), 41 | GetUser: usecases.NewGetUserHandler(userRepo, sessionRepo, logger), 42 | }, nil 43 | } 44 | 45 | func NewLogger(logLevel string) (*logrus.Entry, error) { 46 | logger := logrus.New() 47 | 48 | level, err := logrus.ParseLevel(logLevel) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | logger.SetLevel(level) 54 | 55 | logger.SetFormatter(&logrus.TextFormatter{ 56 | FullTimestamp: true, 57 | TimestampFormat: time.StampMilli, 58 | }) 59 | 60 | return logrus.NewEntry(logger), nil 61 | } 62 | -------------------------------------------------------------------------------- /backend/drive/adapters/file_model.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/domain" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | type FileModel struct { 10 | Id string `gorm:"size:36;primaryKey"` 11 | DriveId string `gorm:"size:36;index:idx_drive_parent_name,unique,priority:1"` 12 | Parent string `gorm:"size:36;index:idx_drive_parent_name,unique,priority:2"` 13 | Name string `gorm:"size:128;index:idx_drive_parent_name,unique,priority:3"` 14 | State string 15 | LastChange time.Time 16 | Size int 17 | Hash string 18 | DownloadCounts int 19 | } 20 | 21 | func NewFileModel(file *domain.File) FileModel { 22 | return FileModel{ 23 | Id: file.Id().String(), 24 | DriveId: file.DriveId().String(), 25 | Parent: file.Parent().String(), 26 | Name: file.Name(), 27 | Hash: file.Hash(), 28 | State: file.State().Value(), 29 | LastChange: file.LastChange(), 30 | Size: file.Size(), 31 | DownloadCounts: file.DownloadCounts(), 32 | } 33 | } 34 | 35 | func (model FileModel) TableName() string { 36 | return "files" 37 | } 38 | 39 | func (model FileModel) File() (*domain.File, error) { 40 | id, err := uuid.Parse(model.Id) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | driveId, err := uuid.Parse(model.DriveId) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | parent, err := domain.CreateParent(model.Parent) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | state, err := domain.CreateState(model.State) 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return domain.NewFileFromRepository( 61 | id, 62 | model.Size, 63 | model.Hash, 64 | model.DownloadCounts, 65 | driveId, 66 | parent, 67 | model.Name, 68 | state, 69 | model.LastChange, 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/views/drive/Search.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 53 | 54 | 75 | -------------------------------------------------------------------------------- /backend/drive/domain/parent.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | var ( 9 | ErrInvalidParent = errors.New("invalid parent") 10 | ) 11 | 12 | var ( 13 | ParentKindDrive = "Drive" 14 | ParentRecycleBin = "RecycleBin" 15 | ParentKindFolder = "Folder" 16 | ) 17 | 18 | type Parent struct { 19 | kind string 20 | folderId uuid.UUID 21 | } 22 | 23 | func CreateDriveParent() Parent { 24 | return Parent{ 25 | kind: ParentKindDrive, 26 | } 27 | } 28 | 29 | func CreateRecycleBinParent() Parent { 30 | return Parent{ 31 | kind: ParentRecycleBin, 32 | } 33 | } 34 | 35 | func CreateFolderParent(folderId uuid.UUID) (Parent, error) { 36 | if folderId == uuid.Nil { 37 | return Parent{}, ErrInvalidParent 38 | } 39 | 40 | return Parent{kind: ParentKindFolder, folderId: folderId}, nil 41 | } 42 | 43 | func CreateParent(value string) (Parent, error) { 44 | if value == ParentKindDrive { 45 | return Parent{kind: ParentKindDrive}, nil 46 | } 47 | if value == ParentRecycleBin { 48 | return Parent{kind: ParentRecycleBin}, nil 49 | } 50 | 51 | folderId, err := uuid.Parse(value) 52 | if err != nil { 53 | return Parent{}, ErrInvalidParent 54 | } 55 | 56 | return Parent{kind: ParentKindFolder, folderId: folderId}, nil 57 | } 58 | 59 | func (parent Parent) IsZero() bool { 60 | return parent == Parent{} 61 | } 62 | 63 | func (parent Parent) String() string { 64 | if parent.folderId == uuid.Nil { 65 | return parent.kind 66 | } 67 | 68 | return parent.folderId.String() 69 | } 70 | 71 | func (parent Parent) IsDrive() bool { 72 | return parent.kind == ParentKindDrive 73 | } 74 | 75 | func (parent Parent) IsRecycleBin() bool { 76 | return parent.kind == ParentRecycleBin 77 | } 78 | 79 | func (parent Parent) IsFolder() bool { 80 | return parent.kind == ParentKindFolder 81 | } 82 | 83 | func (parent Parent) FolderId() uuid.UUID { 84 | return parent.folderId 85 | } 86 | -------------------------------------------------------------------------------- /frontend/src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 39 | 40 | -------------------------------------------------------------------------------- /backend/storage/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/storage/adapters" 5 | "github.com/axli-personal/drive/backend/storage/usecases" 6 | "github.com/sirupsen/logrus" 7 | "time" 8 | ) 9 | 10 | type Config struct { 11 | Endpoint string `env:"ENDPOINT" yaml:"endpoint"` 12 | DataDirectory string `env:"DATA_DIRECTORY" yaml:"data-directory"` 13 | RequestPerSecond int `env:"REQUEST_PER_SECOND" yaml:"request-per-second"` 14 | DriveServiceAddress string `env:"DRIVE_SERVICE_ADDRESS" yaml:"drive-service-address"` 15 | RedisConnectionString string `env:"REDIS_CONNECTION_STRING" yaml:"redis-connection-string"` 16 | LogLevel string `env:"LOG_LEVEL" yaml:"log-level"` 17 | } 18 | 19 | type Service struct { 20 | UploadObject usecases.UploadHandler 21 | DownloadObject usecases.DownloadHandler 22 | } 23 | 24 | func NewService(config Config) (Service, error) { 25 | driveService, err := adapters.NewRPCDriveService(config.DriveServiceAddress) 26 | if err != nil { 27 | return Service{}, err 28 | } 29 | 30 | objectRepo, err := adapters.NewDiskObjectRepository(config.DataDirectory) 31 | if err != nil { 32 | return Service{}, err 33 | } 34 | 35 | logger, err := NewLogger(config.LogLevel) 36 | if err != nil { 37 | return Service{}, err 38 | } 39 | 40 | return Service{ 41 | UploadObject: usecases.NewUploadHandler(driveService, objectRepo, logger), 42 | DownloadObject: usecases.NewDownloadHandler(driveService, objectRepo, logger), 43 | }, nil 44 | } 45 | 46 | func NewLogger(logLevel string) (*logrus.Entry, error) { 47 | logger := logrus.New() 48 | 49 | level, err := logrus.ParseLevel(logLevel) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | logger.SetLevel(level) 55 | 56 | logger.SetFormatter(&logrus.JSONFormatter{ 57 | TimestampFormat: time.StampMilli, 58 | }) 59 | 60 | return logrus.NewEntry(logger), nil 61 | } 62 | -------------------------------------------------------------------------------- /backend/user/usecases/get_user.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/pkg/errors" 7 | "github.com/axli-personal/drive/backend/user/domain" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | var ( 13 | ErrCodeUseCase = "UseCase" 14 | ErrCodeNotLogin = "NotLogin" 15 | ) 16 | 17 | type GetUserArgs struct { 18 | SessionId uuid.UUID 19 | } 20 | 21 | type GetUserResult struct { 22 | Account domain.Account 23 | Username string 24 | Introduction string 25 | } 26 | 27 | type getUserHandler struct { 28 | userRepo domain.UserRepository 29 | sessionRepo domain.SessionRepository 30 | } 31 | 32 | func (handler getUserHandler) Handle(ctx context.Context, args GetUserArgs) (result GetUserResult, err error) { 33 | session, err := handler.sessionRepo.GetSession(ctx, args.SessionId) 34 | if err != nil { 35 | if err, ok := err.(*errors.Error); ok { 36 | if err.Code() == domain.ErrCodeNotFound { 37 | return GetUserResult{}, errors.New(ErrCodeNotLogin, "please login first", err) 38 | } 39 | } 40 | return GetUserResult{}, errors.New(ErrCodeUseCase, "fail to get user", err) 41 | } 42 | 43 | user, err := handler.userRepo.GetUser(ctx, session.Account()) 44 | if err != nil { 45 | return GetUserResult{}, err 46 | } 47 | 48 | return GetUserResult{ 49 | Account: user.Account(), 50 | Username: user.Username(), 51 | Introduction: user.Introduction(), 52 | }, nil 53 | } 54 | 55 | type GetUserHandler decorator.Handler[GetUserArgs, GetUserResult] 56 | 57 | func NewGetUserHandler( 58 | userRepo domain.UserRepository, 59 | sessionRepo domain.SessionRepository, 60 | logger *logrus.Entry, 61 | ) GetUserHandler { 62 | return decorator.WithLogging[GetUserArgs, GetUserResult]( 63 | getUserHandler{ 64 | userRepo: userRepo, 65 | sessionRepo: sessionRepo, 66 | }, 67 | logger, 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /backend/drive/usecases/remove_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/remote" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | RemoveFileArgs struct { 14 | SessionId string 15 | FileId uuid.UUID 16 | } 17 | 18 | RemoveFileResult struct { 19 | } 20 | ) 21 | 22 | type removeFileHandler struct { 23 | repo repository.Repository 24 | userService remote.UserService 25 | } 26 | 27 | func (h removeFileHandler) Handle(ctx context.Context, args RemoveFileArgs) (RemoveFileResult, error) { 28 | user, err := h.userService.GetUser(ctx, args.SessionId) 29 | if err != nil { 30 | return RemoveFileResult{}, err 31 | } 32 | 33 | drive, err := h.repo.GetDriveRepo().GetDriveByOwner(ctx, user.Account()) 34 | if err != nil { 35 | return RemoveFileResult{}, err 36 | } 37 | 38 | file, err := h.repo.GetFileRepo().GetFile(ctx, args.FileId) 39 | if err != nil { 40 | return RemoveFileResult{}, err 41 | } 42 | 43 | err = file.CanWrite(drive.Id()) 44 | if err != nil { 45 | return RemoveFileResult{}, err 46 | } 47 | 48 | err = file.ManuallyTrash() 49 | if err != nil { 50 | return RemoveFileResult{}, err 51 | } 52 | 53 | err = h.repo.GetFileRepo().UpdateFile(ctx, file, repository.UpdateFileOptions{}) 54 | if err != nil { 55 | return RemoveFileResult{}, err 56 | } 57 | 58 | return RemoveFileResult{}, nil 59 | } 60 | 61 | type RemoveFileHandler decorator.Handler[RemoveFileArgs, RemoveFileResult] 62 | 63 | func NewRemoveFileHandler( 64 | repo repository.Repository, 65 | userService remote.UserService, 66 | logger *logrus.Entry, 67 | ) RemoveFileHandler { 68 | return decorator.WithLogging[RemoveFileArgs, RemoveFileResult]( 69 | removeFileHandler{ 70 | repo: repo, 71 | userService: userService, 72 | }, 73 | logger, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /backend/drive/usecases/restore_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/remote" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | RestoreFileArgs struct { 14 | SessionId string 15 | FileId uuid.UUID 16 | } 17 | 18 | RestoreFileResult struct { 19 | } 20 | ) 21 | 22 | type restoreFileHandler struct { 23 | repo repository.Repository 24 | userService remote.UserService 25 | } 26 | 27 | func (h restoreFileHandler) Handle(ctx context.Context, args RestoreFileArgs) (RestoreFileResult, error) { 28 | user, err := h.userService.GetUser(ctx, args.SessionId) 29 | if err != nil { 30 | return RestoreFileResult{}, err 31 | } 32 | 33 | drive, err := h.repo.GetDriveRepo().GetDriveByOwner(ctx, user.Account()) 34 | if err != nil { 35 | return RestoreFileResult{}, err 36 | } 37 | 38 | file, err := h.repo.GetFileRepo().GetFile(ctx, args.FileId) 39 | if err != nil { 40 | return RestoreFileResult{}, err 41 | } 42 | 43 | err = file.CanWrite(drive.Id()) 44 | if err != nil { 45 | return RestoreFileResult{}, err 46 | } 47 | 48 | err = file.ManuallyRestore() 49 | if err != nil { 50 | return RestoreFileResult{}, err 51 | } 52 | 53 | err = h.repo.GetFileRepo().UpdateFile(ctx, file, repository.UpdateFileOptions{}) 54 | if err != nil { 55 | return RestoreFileResult{}, err 56 | } 57 | 58 | return RestoreFileResult{}, nil 59 | } 60 | 61 | type RestoreFileHandler decorator.Handler[RestoreFileArgs, RestoreFileResult] 62 | 63 | func NewRestoreFileHandler( 64 | repo repository.Repository, 65 | userService remote.UserService, 66 | logger *logrus.Entry, 67 | ) RestoreFileHandler { 68 | return decorator.WithLogging[RestoreFileArgs, RestoreFileResult]( 69 | restoreFileHandler{ 70 | repo: repo, 71 | userService: userService, 72 | }, 73 | logger, 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /frontend/src/css/highlight.scss: -------------------------------------------------------------------------------- 1 | /* Specific modification */ 2 | pre code { 3 | padding: 1em; 4 | display: block; 5 | overflow-x: auto; 6 | background: #ffffff; 7 | border: 3px solid #ececec; 8 | } 9 | 10 | code { 11 | padding: 0 2px; 12 | font-size: 16px; 13 | color: #24292e; 14 | background: #f3f4f4; 15 | border: 1px solid #e7eaed; 16 | border-radius: 3px; 17 | } 18 | 19 | /* Official github theme */ 20 | 21 | .hljs-doctag, 22 | .hljs-keyword, 23 | .hljs-meta .hljs-keyword, 24 | .hljs-template-tag, 25 | .hljs-template-variable, 26 | .hljs-type, 27 | .hljs-variable.language_ { 28 | color: #d73a49; 29 | } 30 | 31 | .hljs-title, 32 | .hljs-title.class_, 33 | .hljs-title.class_.inherited__, 34 | .hljs-title.function_ { 35 | color: #6f42c1; 36 | } 37 | 38 | .hljs-attr, 39 | .hljs-attribute, 40 | .hljs-literal, 41 | .hljs-meta, 42 | .hljs-number, 43 | .hljs-operator, 44 | .hljs-selector-attr, 45 | .hljs-selector-class, 46 | .hljs-selector-id, 47 | .hljs-variable { 48 | color: #005cc5; 49 | } 50 | 51 | .hljs-meta .hljs-string, 52 | .hljs-regexp, 53 | .hljs-string { 54 | color: #032f62; 55 | } 56 | 57 | .hljs-built_in, 58 | .hljs-symbol { 59 | color: #e36209; 60 | } 61 | 62 | .hljs-code, 63 | .hljs-comment, 64 | .hljs-formula { 65 | color: #6a737d; 66 | } 67 | 68 | .hljs-name, 69 | .hljs-quote, 70 | .hljs-selector-pseudo, 71 | .hljs-selector-tag { 72 | color: #22863a; 73 | } 74 | 75 | .hljs-subst { 76 | color: #24292e; 77 | } 78 | 79 | .hljs-section { 80 | color: #005cc5; 81 | font-weight: 700; 82 | } 83 | 84 | .hljs-bullet { 85 | color: #735c0f; 86 | } 87 | 88 | .hljs-emphasis { 89 | color: #24292e; 90 | font-style: italic; 91 | } 92 | 93 | .hljs-strong { 94 | color: #24292e; 95 | font-weight: 700; 96 | } 97 | 98 | .hljs-addition { 99 | color: #22863a; 100 | background-color: #f0fff4; 101 | } 102 | 103 | .hljs-deletion { 104 | color: #b31d28; 105 | background-color: #ffeef0; 106 | } -------------------------------------------------------------------------------- /backend/drive/ports/get_recycle_bin.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/auth" 6 | "github.com/axli-personal/drive/backend/drive/usecases" 7 | "github.com/axli-personal/drive/backend/pkg/errors" 8 | "github.com/axli-personal/drive/backend/pkg/types" 9 | "github.com/gofiber/fiber/v2" 10 | ) 11 | 12 | func (server HTTPServer) GetRecycleBin(ctx *fiber.Ctx) (err error) { 13 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 14 | if sessionId == "" { 15 | return ctx.Status(fiber.StatusForbidden).JSON( 16 | types.ErrorResponse{ 17 | Code: types.ErrCodeUnauthenticated, 18 | Message: "please login first", 19 | Detail: "missing session cookie", 20 | }, 21 | ) 22 | } 23 | 24 | result, err := server.svc.GetRecycleBin.Handle( 25 | context.Background(), 26 | usecases.GetRecycleBinArgs{ 27 | SessionId: sessionId, 28 | }, 29 | ) 30 | if err != nil { 31 | if err, ok := err.(*errors.Error); ok { 32 | errResponse := types.ErrorResponse{ 33 | Code: err.Code(), 34 | Message: err.Message(), 35 | Detail: err.Error(), 36 | } 37 | if err.Code() == types.ErrCodeUnauthenticated { 38 | return ctx.Status(fiber.StatusForbidden).JSON(errResponse) 39 | } 40 | if err.Code() == usecases.ErrCodeNotCreateDrive { 41 | return ctx.Status(fiber.StatusNotFound).JSON(errResponse) 42 | } 43 | return ctx.Status(fiber.StatusInternalServerError).JSON(errResponse) 44 | } 45 | return err 46 | } 47 | 48 | response := types.GetRecycleBinResponse{ 49 | Children: types.Children{}, 50 | } 51 | 52 | for _, link := range result.Children.Folders { 53 | response.Children.Folders = append(response.Children.Folders, types.FolderLink{ 54 | Id: link.Id.String(), 55 | Name: link.Name, 56 | }) 57 | } 58 | 59 | for _, link := range result.Children.Files { 60 | response.Children.Files = append(response.Children.Files, types.FileLink{ 61 | Id: link.Id.String(), 62 | Name: link.Name, 63 | Bytes: link.Bytes, 64 | }) 65 | } 66 | 67 | return ctx.JSON(response) 68 | } 69 | -------------------------------------------------------------------------------- /backend/user/adapters/session_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/pkg/errors" 6 | "github.com/axli-personal/drive/backend/user/domain" 7 | "github.com/google/uuid" 8 | "github.com/redis/go-redis/v9" 9 | "github.com/sirupsen/logrus" 10 | "time" 11 | ) 12 | 13 | const ( 14 | SessionPrefix = "session:" 15 | ) 16 | 17 | type SessionRepository struct { 18 | logger *logrus.Entry 19 | client *redis.Client 20 | } 21 | 22 | func NewSessionRepository(connectionString string, logger *logrus.Entry) (SessionRepository, error) { 23 | options, err := redis.ParseURL(connectionString) 24 | if err != nil { 25 | return SessionRepository{}, err 26 | } 27 | 28 | client := redis.NewClient(options) 29 | 30 | err = client.Ping(context.Background()).Err() 31 | if err != nil { 32 | return SessionRepository{}, err 33 | } 34 | 35 | return SessionRepository{ 36 | logger: logger, 37 | client: client, 38 | }, nil 39 | } 40 | 41 | func (repo SessionRepository) SaveSession(ctx context.Context, session *domain.Session, expire time.Duration) error { 42 | model := NewSessionModel(session) 43 | 44 | err := repo.client.HSet(ctx, SessionPrefix+model.Id, &model).Err() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | err = repo.client.Expire(ctx, SessionPrefix+model.Id, expire).Err() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (repo SessionRepository) GetSession(ctx context.Context, id uuid.UUID) (*domain.Session, error) { 58 | model := SessionModel{Id: id.String()} 59 | 60 | result := repo.client.HGetAll(ctx, SessionPrefix+model.Id) 61 | if result.Err() != nil { 62 | return nil, errors.New(domain.ErrCodeRepository, "fail to get session", result.Err()) 63 | } 64 | 65 | if len(result.Val()) == 0 { 66 | return nil, errors.New(domain.ErrCodeNotFound, "session not found", nil) 67 | } 68 | 69 | err := result.Scan(&model) 70 | if err != nil { 71 | return nil, errors.New(domain.ErrCodeUnmarshal, "fail to scan session", err) 72 | } 73 | 74 | return model.Session() 75 | } 76 | -------------------------------------------------------------------------------- /test/src/performance.js: -------------------------------------------------------------------------------- 1 | import http from 'k6/http'; 2 | import { check } from "k6"; 3 | import { randomString, statusCheck } from "./utils.js"; 4 | 5 | export const options = { 6 | vus: 500, 7 | duration: "30s" 8 | }; 9 | 10 | const USER_SERVICE_URL = "http://127.0.0.1:8080"; 11 | const DRIVE_SERVICE_URL = "http://127.0.0.1:8081"; 12 | const STORAGE_SERVICE_URL = "http://127.0.0.1:8082"; 13 | 14 | const jsonHeaders = { 'Content-Type': 'application/json' }; 15 | const cookies = { 'SID': '' }; 16 | const smallFile = open('small.txt', 'b'); 17 | 18 | export default function () { 19 | let response; 20 | 21 | const account = randomString(15); 22 | const username = randomString(15); 23 | const password = randomString(15); 24 | 25 | const registerBody = JSON.stringify({ 26 | account, 27 | username, 28 | password, 29 | }); 30 | 31 | const loginBody = JSON.stringify({ 32 | account, 33 | password, 34 | }) 35 | 36 | const uploadBody = { 37 | parent: "Drive", 38 | file: http.file(smallFile, 'small.txt') 39 | } 40 | 41 | response = http.post(`${USER_SERVICE_URL}/register`, registerBody, { headers: jsonHeaders }); 42 | check(response, { 'register': statusCheck }); 43 | 44 | response = http.post(`${USER_SERVICE_URL}/login`, loginBody, { headers: jsonHeaders }); 45 | check(response, { 'login': statusCheck }); 46 | 47 | cookies.SID = response.cookies.SID[0].value; 48 | 49 | response = http.post(`${DRIVE_SERVICE_URL}/drive/create`, null, { headers: jsonHeaders, cookies }); 50 | check(response, { 'create drive': statusCheck }); 51 | 52 | response = http.get(`${DRIVE_SERVICE_URL}/drive`, { headers: jsonHeaders, cookies }); 53 | check(response, { 'get drive': statusCheck }); 54 | 55 | response = http.post(`${STORAGE_SERVICE_URL}/upload`, uploadBody, { cookies }); 56 | check(response, { 'upload': statusCheck }); 57 | 58 | response = http.get(`${DRIVE_SERVICE_URL}/drive`, { headers: jsonHeaders, cookies }); 59 | check(response, { 'get drive': statusCheck }); 60 | } 61 | -------------------------------------------------------------------------------- /backend/drive/usecases/share_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/remote" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | ShareFileArgs struct { 14 | SessionId string 15 | FileId uuid.UUID 16 | } 17 | 18 | ShareFileResult struct { 19 | } 20 | ) 21 | 22 | type shareFileHandler struct { 23 | userService remote.UserService 24 | driveRepo repository.DriveRepository 25 | fileRepo repository.FileRepository 26 | } 27 | 28 | func (handler shareFileHandler) Handle(ctx context.Context, args ShareFileArgs) (ShareFileResult, error) { 29 | user, err := handler.userService.GetUser(ctx, args.SessionId) 30 | if err != nil { 31 | return ShareFileResult{}, err 32 | } 33 | 34 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 35 | if err != nil { 36 | return ShareFileResult{}, err 37 | } 38 | 39 | file, err := handler.fileRepo.GetFile(ctx, args.FileId) 40 | if err != nil { 41 | return ShareFileResult{}, err 42 | } 43 | 44 | err = file.CanWrite(drive.Id()) 45 | if err != nil { 46 | return ShareFileResult{}, err 47 | } 48 | 49 | err = file.Share() 50 | if err != nil { 51 | return ShareFileResult{}, err 52 | } 53 | 54 | err = handler.fileRepo.UpdateFile(ctx, file, repository.UpdateFileOptions{}) 55 | if err != nil { 56 | return ShareFileResult{}, err 57 | } 58 | 59 | return ShareFileResult{}, nil 60 | } 61 | 62 | type ShareFileHandler decorator.Handler[ShareFileArgs, ShareFileResult] 63 | 64 | func NewShareFileHandler( 65 | userService remote.UserService, 66 | driveRepo repository.DriveRepository, 67 | fileRepo repository.FileRepository, 68 | logger *logrus.Entry, 69 | ) ShareFileHandler { 70 | return decorator.WithLogging[ShareFileArgs, ShareFileResult]( 71 | shareFileHandler{ 72 | userService: userService, 73 | driveRepo: driveRepo, 74 | fileRepo: fileRepo, 75 | }, 76 | logger, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /backend/drive/ports/get_drive.go: -------------------------------------------------------------------------------- 1 | package ports 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/common/auth" 5 | "github.com/axli-personal/drive/backend/drive/usecases" 6 | "github.com/axli-personal/drive/backend/pkg/errors" 7 | "github.com/axli-personal/drive/backend/pkg/types" 8 | "github.com/gofiber/fiber/v2" 9 | ) 10 | 11 | func (server HTTPServer) GetDrive(ctx *fiber.Ctx) (err error) { 12 | sessionId := ctx.Cookies(auth.SessionIdCookieKey) 13 | if sessionId == "" { 14 | return ctx.Status(fiber.StatusForbidden).JSON( 15 | types.ErrorResponse{ 16 | Code: types.ErrCodeUnauthenticated, 17 | Message: "please login first", 18 | Detail: "missing session cookie", 19 | }, 20 | ) 21 | } 22 | 23 | result, err := server.svc.GetDrive.Handle( 24 | ctx.Context(), 25 | usecases.GetDriveArgs{ 26 | SessionId: sessionId, 27 | }) 28 | if err != nil { 29 | if err, ok := err.(*errors.Error); ok { 30 | errResponse := types.ErrorResponse{ 31 | Code: err.Code(), 32 | Message: err.Message(), 33 | Detail: err.Error(), 34 | } 35 | if err.Code() == types.ErrCodeUnauthenticated { 36 | return ctx.Status(fiber.StatusForbidden).JSON(errResponse) 37 | } 38 | if err.Code() == usecases.ErrCodeNotCreateDrive { 39 | return ctx.Status(fiber.StatusNotFound).JSON(errResponse) 40 | } 41 | return ctx.Status(fiber.StatusInternalServerError).JSON(errResponse) 42 | } 43 | return err 44 | } 45 | 46 | response := types.GetDriveResponse{ 47 | DriveId: result.Id.String(), 48 | Children: types.Children{}, 49 | PlanName: result.Plan.Name(), 50 | UsedBytes: result.Usage.Bytes(), 51 | MaxBytes: result.Plan.MaxBytes(), 52 | } 53 | 54 | for _, link := range result.Children.Folders { 55 | response.Children.Folders = append(response.Children.Folders, types.FolderLink{ 56 | Id: link.Id.String(), 57 | Name: link.Name, 58 | }) 59 | } 60 | 61 | for _, link := range result.Children.Files { 62 | response.Children.Files = append(response.Children.Files, types.FileLink{ 63 | Id: link.Id.String(), 64 | Name: link.Name, 65 | Bytes: link.Bytes, 66 | }) 67 | } 68 | 69 | return ctx.JSON(response) 70 | } 71 | -------------------------------------------------------------------------------- /backend/storage/usecases/upload.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/pkg/types" 7 | "github.com/axli-personal/drive/backend/storage/domain" 8 | "github.com/axli-personal/drive/backend/storage/remote" 9 | "github.com/axli-personal/drive/backend/storage/repository" 10 | "github.com/sirupsen/logrus" 11 | "io" 12 | ) 13 | 14 | type ( 15 | UploadArgs struct { 16 | SessionId string 17 | FileParent string 18 | FileName string 19 | Data io.ReadSeeker 20 | } 21 | 22 | UploadResult struct { 23 | } 24 | ) 25 | 26 | type uploadHandler struct { 27 | driveService remote.DriveService 28 | objectRepo repository.ObjectRepository 29 | capacityRepo repository.CapacityRepository 30 | } 31 | 32 | func (handler uploadHandler) Handle(ctx context.Context, args UploadArgs) (UploadResult, error) { 33 | object, err := domain.NewObject(args.Data) 34 | if err != nil { 35 | return UploadResult{}, err 36 | } 37 | 38 | startResponse, err := handler.driveService.StartUpload( 39 | types.StartUploadRequest{ 40 | SessionId: args.SessionId, 41 | FileParent: args.FileParent, 42 | FileName: args.FileName, 43 | FileHash: object.Hash(), 44 | FileSize: object.TotalBytes(), 45 | }, 46 | ) 47 | if err != nil { 48 | return UploadResult{}, err 49 | } 50 | 51 | err = handler.objectRepo.SaveObject(ctx, object) 52 | if err != nil { 53 | return UploadResult{}, err 54 | } 55 | 56 | _, err = handler.driveService.FinishUpload( 57 | types.FinishUploadRequest{ 58 | FileId: startResponse.FileId, 59 | }, 60 | ) 61 | if err != nil { 62 | return UploadResult{}, err 63 | } 64 | 65 | return UploadResult{}, nil 66 | } 67 | 68 | type UploadHandler decorator.Handler[UploadArgs, UploadResult] 69 | 70 | func NewUploadHandler( 71 | driveService remote.DriveService, 72 | objectRepo repository.ObjectRepository, 73 | logger *logrus.Entry, 74 | ) UploadHandler { 75 | return decorator.WithLogging[UploadArgs, UploadResult]( 76 | uploadHandler{ 77 | driveService: driveService, 78 | objectRepo: objectRepo, 79 | }, 80 | logger, 81 | ) 82 | } 83 | -------------------------------------------------------------------------------- /backend/channel/usecases/join_channel.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/axli-personal/drive/backend/channel/repository" 7 | "github.com/axli-personal/drive/backend/common/decorator" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | const ( 14 | ErrCodeInvalidInviteToken = "InvalidInviteToken" 15 | ) 16 | 17 | type ( 18 | JoinChannelArgs struct { 19 | UserAccount string 20 | ChannelId uuid.UUID 21 | InviteToken string 22 | } 23 | 24 | JoinChannelResult struct { 25 | } 26 | ) 27 | 28 | type joinChannelHandler struct { 29 | channelRepo repository.ChannelRepository 30 | memberRepo repository.MemberRepository 31 | } 32 | 33 | func (h joinChannelHandler) Handle(ctx context.Context, args JoinChannelArgs) (JoinChannelResult, error) { 34 | channel, err := h.channelRepo.GetChannel(ctx, args.ChannelId) 35 | if err != nil { 36 | return JoinChannelResult{}, errors.New(ErrCodeChannelNotFound, "fail to join channel", err) 37 | } 38 | 39 | if args.InviteToken != channel.InviteToken { 40 | return JoinChannelResult{}, errors.New(ErrCodeInvalidInviteToken, "fail to join channel", err) 41 | } 42 | 43 | member, err := domain.NewChannelMember(args.ChannelId, args.UserAccount, domain.RoleMember) 44 | if err != nil { 45 | return JoinChannelResult{}, errors.New(ErrCodeInternal, "fail to join channel", err) 46 | } 47 | 48 | err = h.memberRepo.SaveMember(ctx, member) 49 | if err != nil { 50 | return JoinChannelResult{}, errors.New(ErrCodeInternal, "fail to join channel", err) 51 | } 52 | 53 | return JoinChannelResult{}, nil 54 | } 55 | 56 | type JoinChannelHandler decorator.Handler[JoinChannelArgs, JoinChannelResult] 57 | 58 | func NewJoinChannelHandler( 59 | channelRepo repository.ChannelRepository, 60 | memberRepo repository.MemberRepository, 61 | logger *logrus.Entry, 62 | ) JoinChannelHandler { 63 | return decorator.WithLogging[JoinChannelArgs, JoinChannelResult]( 64 | joinChannelHandler{ 65 | channelRepo: channelRepo, 66 | memberRepo: memberRepo, 67 | }, 68 | logger, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /backend/drive/domain/file.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | "time" 7 | ) 8 | 9 | type File struct { 10 | id uuid.UUID 11 | size int 12 | hash string 13 | downloadCounts int 14 | Metadata 15 | } 16 | 17 | var ( 18 | ErrInvalidUUID = errors.New("invalid uuid") 19 | ErrCannotIncreaseDownloadCount = errors.New("cannot increase download count") 20 | ) 21 | 22 | func NewFile(driveId uuid.UUID, fileParent Parent, fileName string, fileSize int, fileHash string) (*File, error) { 23 | if driveId == uuid.Nil { 24 | return nil, ErrInvalidUUID 25 | } 26 | if fileParent.IsZero() { 27 | return nil, ErrInvalidParent 28 | } 29 | 30 | return &File{ 31 | id: uuid.New(), 32 | Metadata: Metadata{ 33 | driveId: driveId, 34 | parent: fileParent, 35 | name: fileName, 36 | state: StateLocked, 37 | lastChange: time.Now(), 38 | }, 39 | size: fileSize, 40 | hash: fileHash, 41 | downloadCounts: 0, 42 | }, nil 43 | } 44 | 45 | func NewFileFromRepository( 46 | id uuid.UUID, 47 | size int, 48 | hash string, 49 | downloadCounts int, 50 | driveId uuid.UUID, 51 | parent Parent, 52 | name string, 53 | state State, 54 | lastChange time.Time, 55 | ) (*File, error) { 56 | return &File{ 57 | id: id, 58 | size: size, 59 | hash: hash, 60 | downloadCounts: downloadCounts, 61 | Metadata: Metadata{ 62 | driveId: driveId, 63 | parent: parent, 64 | name: name, 65 | state: state, 66 | lastChange: lastChange, 67 | }, 68 | }, nil 69 | } 70 | 71 | func (file *File) Id() uuid.UUID { 72 | return file.id 73 | } 74 | 75 | func (file *File) Size() int { 76 | return file.size 77 | } 78 | 79 | func (file *File) SetSize(size int) { 80 | file.size = size 81 | } 82 | 83 | func (file *File) Hash() string { 84 | return file.hash 85 | } 86 | 87 | func (file *File) SetHash(hash string) { 88 | file.hash = hash 89 | } 90 | 91 | func (file *File) DownloadCounts() int { 92 | return file.downloadCounts 93 | } 94 | 95 | func (file *File) IncreaseDownloadCounts() { 96 | file.downloadCounts += 1 97 | } 98 | -------------------------------------------------------------------------------- /backend/drive/usecases/share_folder.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/remote" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | ShareFolderArgs struct { 14 | SessionId string 15 | FolderId uuid.UUID 16 | } 17 | 18 | ShareFolderResult struct { 19 | } 20 | ) 21 | 22 | type shareFolderHandler struct { 23 | userService remote.UserService 24 | driveRepo repository.DriveRepository 25 | folderRepo repository.FolderRepository 26 | } 27 | 28 | func (handler shareFolderHandler) Handle(ctx context.Context, args ShareFolderArgs) (ShareFolderResult, error) { 29 | user, err := handler.userService.GetUser(ctx, args.SessionId) 30 | if err != nil { 31 | return ShareFolderResult{}, err 32 | } 33 | 34 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 35 | if err != nil { 36 | return ShareFolderResult{}, err 37 | } 38 | 39 | folder, err := handler.folderRepo.GetFolder(ctx, args.FolderId) 40 | if err != nil { 41 | return ShareFolderResult{}, err 42 | } 43 | 44 | err = folder.CanWrite(drive.Id()) 45 | if err != nil { 46 | return ShareFolderResult{}, err 47 | } 48 | 49 | err = folder.Share() 50 | if err != nil { 51 | return ShareFolderResult{}, err 52 | } 53 | 54 | err = handler.folderRepo.UpdateFolder(ctx, folder, repository.UpdateFolderOptions{UpdateChildrenState: true}) 55 | if err != nil { 56 | return ShareFolderResult{}, err 57 | } 58 | 59 | return ShareFolderResult{}, nil 60 | } 61 | 62 | type ShareFolderHandler decorator.Handler[ShareFolderArgs, ShareFolderResult] 63 | 64 | func NewShareFolderHandler( 65 | userService remote.UserService, 66 | driveRepo repository.DriveRepository, 67 | folderRepo repository.FolderRepository, 68 | logger *logrus.Entry, 69 | ) ShareFolderHandler { 70 | return decorator.WithLogging[ShareFolderArgs, ShareFolderResult]( 71 | shareFolderHandler{ 72 | userService: userService, 73 | driveRepo: driveRepo, 74 | folderRepo: folderRepo, 75 | }, 76 | logger, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /backend/storage/adapters/object_repository_test.go: -------------------------------------------------------------------------------- 1 | package adapters_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 7 | "github.com/axli-personal/drive/backend/storage/adapters" 8 | "github.com/axli-personal/drive/backend/storage/domain" 9 | "io" 10 | "os" 11 | "path" 12 | "testing" 13 | ) 14 | 15 | func TestDiskObjectRepository(t *testing.T) { 16 | testDir := path.Join(os.TempDir(), "drive-test") 17 | 18 | repo, err := adapters.NewDiskObjectRepository(testDir) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | content := bytes.Repeat([]byte("text content\n"), 100) 24 | data := bytes.NewReader(content) 25 | 26 | object, err := domain.NewObject(data) 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | err = repo.SaveObject(context.Background(), object) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | 36 | ObjectInRepo, err := repo.GetObject(context.Background(), object.Hash()) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | contentInRepo, err := io.ReadAll(ObjectInRepo) 42 | if err != nil { 43 | t.Fatal(err) 44 | } 45 | 46 | if bytes.Compare(content, contentInRepo) != 0 { 47 | t.Fatal("content not equal") 48 | } 49 | } 50 | 51 | func TestCloudObjectRepository(t *testing.T) { 52 | content := bytes.Repeat([]byte("text content\n"), 100) 53 | data := bytes.NewReader(content) 54 | 55 | object, err := domain.NewObject(data) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | endpoint := "oss-cn-hangzhou.aliyuncs.com" 61 | id := "LTAI5t7AVpkHzf81emXHdKFo" 62 | secret := "TZWv7aTpD9c5HhRUSmexqXRKb7VpqU" 63 | 64 | client, err := oss.New(endpoint, id, secret) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | 69 | bucket, err := client.Bucket("mintul") 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | 74 | uploadSession, err := bucket.InitiateMultipartUpload(object.Hash()) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | var parts []oss.UploadPart 80 | part, err := bucket.UploadPart(uploadSession, object, int64(object.TotalBytes()), 1) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | parts = append(parts, part) 85 | 86 | bucket.CompleteMultipartUpload(uploadSession, parts) 87 | } 88 | -------------------------------------------------------------------------------- /backend/drive/usecases/create_folder.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | "time" 12 | ) 13 | 14 | type ( 15 | CreateFolderArgs struct { 16 | SessionId string 17 | FolderParent domain.Parent 18 | FolderName string 19 | } 20 | 21 | CreateFolderResult struct { 22 | FolderId uuid.UUID 23 | FolderName string 24 | LastChange time.Time 25 | } 26 | ) 27 | 28 | type createFolderHandler struct { 29 | userService remote.UserService 30 | driveRepo repository.DriveRepository 31 | folderRepo repository.FolderRepository 32 | } 33 | 34 | func (handler createFolderHandler) Handle(ctx context.Context, args CreateFolderArgs) (CreateFolderResult, error) { 35 | user, err := handler.userService.GetUser(ctx, args.SessionId) 36 | if err != nil { 37 | return CreateFolderResult{}, err 38 | } 39 | 40 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 41 | if err != nil { 42 | return CreateFolderResult{}, err 43 | } 44 | 45 | folder, err := domain.NewFolder(drive.Id(), args.FolderParent, args.FolderName) 46 | if err != nil { 47 | return CreateFolderResult{}, err 48 | } 49 | 50 | err = handler.folderRepo.SaveFolder(ctx, folder) 51 | if err != nil { 52 | return CreateFolderResult{}, err 53 | } 54 | 55 | return CreateFolderResult{ 56 | FolderId: folder.Id(), 57 | FolderName: folder.Name(), 58 | LastChange: folder.LastChange(), 59 | }, nil 60 | } 61 | 62 | type CreateFolderHandler decorator.Handler[CreateFolderArgs, CreateFolderResult] 63 | 64 | func NewCreateFolderHandler( 65 | userService remote.UserService, 66 | driveRepo repository.DriveRepository, 67 | folderRepo repository.FolderRepository, 68 | logger *logrus.Entry, 69 | ) CreateFolderHandler { 70 | return decorator.WithLogging[CreateFolderArgs, CreateFolderResult]( 71 | createFolderHandler{ 72 | userService: userService, 73 | driveRepo: driveRepo, 74 | folderRepo: folderRepo, 75 | }, 76 | logger, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /backend/drive/main/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/axli-personal/drive/backend/drive/ports" 5 | "github.com/axli-personal/drive/backend/drive/service" 6 | "github.com/caarlos0/env/v7" 7 | "github.com/gofiber/fiber/v2" 8 | "github.com/gofiber/fiber/v2/middleware/cors" 9 | "net" 10 | "net/http" 11 | "net/rpc" 12 | "sync" 13 | ) 14 | 15 | func main() { 16 | config := service.Config{} 17 | 18 | err := env.Parse(&config) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | svc, err := service.NewService(config) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | waitGroup := sync.WaitGroup{} 29 | 30 | waitGroup.Add(1) 31 | go func() { 32 | defer waitGroup.Done() 33 | 34 | httpServer := ports.NewHTTPServer(svc) 35 | 36 | app := fiber.New() 37 | 38 | app.Use(cors.New(cors.Config{ 39 | AllowCredentials: true, 40 | })) 41 | 42 | app.Post("/drive/create", httpServer.CreateDrive) 43 | app.Post("/folders/create", httpServer.CreateFolder) 44 | 45 | app.Get("/drive", httpServer.GetDrive) 46 | app.Get("/recycle-bin", httpServer.GetRecycleBin) 47 | 48 | app.Get("/files/:fileId", httpServer.GetFile) 49 | app.Get("/folders/:folderId", httpServer.GetFolder) 50 | app.Get("/path/:parent", httpServer.GetPath) 51 | 52 | app.Post("/files/share/:fileId", httpServer.ShareFile) 53 | app.Post("/folders/share/:folderId", httpServer.ShareFolder) 54 | 55 | app.Post("/files/remove/:fileId", httpServer.RemoveFile) 56 | app.Post("/folders/remove/:folderId", httpServer.RemoveFolder) 57 | 58 | app.Post("/files/restore/:fileId", httpServer.RestoreFile) 59 | app.Post("/folders/restore/:folderId", httpServer.RestoreFolder) 60 | 61 | err := app.Listen(":8080") 62 | if err != nil { 63 | panic(err) 64 | } 65 | }() 66 | 67 | waitGroup.Add(1) 68 | go func() { 69 | defer waitGroup.Done() 70 | 71 | rpcServer := ports.NewRPCServer(svc) 72 | 73 | err := rpc.Register(&rpcServer) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | rpc.HandleHTTP() 79 | 80 | listener, err := net.Listen("tcp", ":8081") 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | err = http.Serve(listener, nil) 86 | if err != nil { 87 | panic(err) 88 | } 89 | }() 90 | 91 | waitGroup.Wait() 92 | } 93 | -------------------------------------------------------------------------------- /backend/drive/usecases/get_path.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type GetPathArgs struct { 14 | SessionId string 15 | Parent domain.Parent 16 | } 17 | 18 | type GetPathResult struct { 19 | Folders []FolderLink 20 | } 21 | 22 | type getPathHandler struct { 23 | userService remote.UserService 24 | driveRepo repository.DriveRepository 25 | folderRepo repository.FolderRepository 26 | } 27 | 28 | func (handler getPathHandler) Handle(ctx context.Context, args GetPathArgs) (GetPathResult, error) { 29 | userDriveId := uuid.Nil 30 | 31 | if args.SessionId != "" { 32 | user, err := handler.userService.GetUser(ctx, args.SessionId) 33 | if err != nil { 34 | return GetPathResult{}, err 35 | } 36 | 37 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 38 | if err != nil { 39 | return GetPathResult{}, err 40 | } 41 | 42 | userDriveId = drive.Id() 43 | } 44 | 45 | result := GetPathResult{} 46 | 47 | currentParent := args.Parent 48 | for currentParent.IsFolder() { 49 | FolderId := currentParent.FolderId() 50 | 51 | folder, err := handler.folderRepo.GetFolder(ctx, FolderId) 52 | if err != nil { 53 | break 54 | } 55 | 56 | err = folder.CanRead(userDriveId) 57 | if err != nil { 58 | break 59 | } 60 | 61 | result.Folders = append(result.Folders, FolderLink{ 62 | Id: folder.Id(), 63 | Name: folder.Name(), 64 | }) 65 | 66 | currentParent = folder.Parent() 67 | } 68 | 69 | return result, nil 70 | } 71 | 72 | type GetPathHandler decorator.Handler[GetPathArgs, GetPathResult] 73 | 74 | func NewGetPathHandler( 75 | userService remote.UserService, 76 | driveRepo repository.DriveRepository, 77 | folderRepo repository.FolderRepository, 78 | logger *logrus.Entry, 79 | ) GetPathHandler { 80 | return decorator.WithLogging[GetPathArgs, GetPathResult]( 81 | getPathHandler{ 82 | userService: userService, 83 | driveRepo: driveRepo, 84 | folderRepo: folderRepo, 85 | }, 86 | logger, 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /backend/drive/adapters/drive_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/drive/domain" 6 | "github.com/axli-personal/drive/backend/drive/repository" 7 | "github.com/axli-personal/drive/backend/pkg/errors" 8 | "github.com/google/uuid" 9 | "gorm.io/driver/mysql" 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type GormDriveRepository struct { 14 | db *gorm.DB 15 | } 16 | 17 | func NewMysqlDriveRepository(connectString string) (repository.DriveRepository, error) { 18 | db, err := gorm.Open(mysql.Open(connectString)) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | err = db.AutoMigrate(&DriveModel{}) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return GormDriveRepository{db: db}, nil 29 | } 30 | 31 | func (repo GormDriveRepository) CreateDrive(ctx context.Context, drive *domain.Drive, options repository.CreateDriveOptions) error { 32 | return repo.db.Transaction(func(tx *gorm.DB) error { 33 | model := NewDriveModel(drive) 34 | 35 | if options.OnlyOneDrive { 36 | err := tx.Where("owner = ?", model.Owner).Take(&DriveModel{}).Error 37 | if err == nil { 38 | return errors.New(repository.ErrCodeRepository, "multiple drive", nil) 39 | } 40 | if err != gorm.ErrRecordNotFound { 41 | return err 42 | } 43 | } 44 | 45 | return repo.db.Create(&model).Error 46 | }) 47 | } 48 | 49 | func (repo GormDriveRepository) GetDrive(ctx context.Context, id uuid.UUID) (*domain.Drive, error) { 50 | model := DriveModel{} 51 | 52 | err := repo.db.Take(&model, "id = ?", id.String()).Error 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return model.Drive() 58 | } 59 | 60 | func (repo GormDriveRepository) GetDriveByOwner(ctx context.Context, owner string) (*domain.Drive, error) { 61 | model := DriveModel{} 62 | 63 | err := repo.db.Take(&model, "owner = ?", owner).Error 64 | if err != nil { 65 | if err == gorm.ErrRecordNotFound { 66 | return nil, errors.New(repository.ErrCodeNotFound, "drive not found", err) 67 | } 68 | return nil, errors.New(repository.ErrCodeRepository, "fail to get drive by owner", err) 69 | } 70 | 71 | return model.Drive() 72 | } 73 | 74 | func (repo GormDriveRepository) UpdateDrive(ctx context.Context, drive *domain.Drive) error { 75 | model := NewDriveModel(drive) 76 | 77 | return repo.db.Save(&model).Error 78 | } 79 | -------------------------------------------------------------------------------- /backend/drive/usecases/delete_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/remote" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/axli-personal/drive/backend/pkg/events" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ( 14 | DeleteFileArgs struct { 15 | SessionId string 16 | FileId uuid.UUID 17 | } 18 | 19 | DeleteFileResult struct { 20 | } 21 | ) 22 | 23 | type deleteFileHandler struct { 24 | userService remote.UserService 25 | driveRepo repository.DriveRepository 26 | fileRepo repository.FileRepository 27 | eventRepo repository.EventRepository 28 | } 29 | 30 | func (handler deleteFileHandler) Handle( 31 | ctx context.Context, 32 | args DeleteFileArgs, 33 | ) (DeleteFileResult, error) { 34 | user, err := handler.userService.GetUser(ctx, args.SessionId) 35 | if err != nil { 36 | return DeleteFileResult{}, err 37 | } 38 | 39 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 40 | if err != nil { 41 | return DeleteFileResult{}, err 42 | } 43 | 44 | file, err := handler.fileRepo.GetFile(ctx, args.FileId) 45 | if err != nil { 46 | return DeleteFileResult{}, err 47 | } 48 | 49 | err = file.CanDelete(drive.Id()) 50 | if err != nil { 51 | return DeleteFileResult{}, err 52 | } 53 | 54 | err = handler.fileRepo.DeleteFile(ctx, file) 55 | if err != nil { 56 | return DeleteFileResult{}, err 57 | } 58 | 59 | // publish may fail. 60 | err = handler.eventRepo.PublishFileDeleted( 61 | ctx, 62 | events.FileDeleted{ 63 | FileId: file.Id().String(), 64 | }, 65 | ) 66 | 67 | return DeleteFileResult{}, nil 68 | } 69 | 70 | type DeleteFileHandler decorator.Handler[DeleteFileArgs, DeleteFileResult] 71 | 72 | func NewDeleteFileHandler( 73 | userService remote.UserService, 74 | driveRepo repository.DriveRepository, 75 | fileRepo repository.FileRepository, 76 | eventRepo repository.EventRepository, 77 | logger *logrus.Entry, 78 | ) DeleteFileHandler { 79 | return decorator.WithLogging[DeleteFileArgs, DeleteFileResult]( 80 | deleteFileHandler{ 81 | userService: userService, 82 | driveRepo: driveRepo, 83 | fileRepo: fileRepo, 84 | eventRepo: eventRepo, 85 | }, 86 | logger, 87 | ) 88 | } 89 | -------------------------------------------------------------------------------- /backend/drive/usecases/get_recycle_bin.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/axli-personal/drive/backend/pkg/errors" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ( 14 | GetRecycleBinArgs struct { 15 | SessionId string 16 | } 17 | 18 | GetRecycleBinResult struct { 19 | Children Children 20 | } 21 | ) 22 | 23 | type getRecycleBinHandler struct { 24 | repo repository.Repository 25 | userService remote.UserService 26 | } 27 | 28 | func (h getRecycleBinHandler) Handle(ctx context.Context, args GetRecycleBinArgs) (GetRecycleBinResult, error) { 29 | user, err := h.userService.GetUser(ctx, args.SessionId) 30 | if err != nil { 31 | return GetRecycleBinResult{}, err 32 | } 33 | 34 | drive, err := h.repo.GetDriveRepo().GetDriveByOwner(ctx, user.Account()) 35 | if err != nil { 36 | if err, ok := err.(*errors.Error); ok { 37 | if err.Code() == repository.ErrCodeNotFound { 38 | return GetRecycleBinResult{}, errors.New(ErrCodeNotCreateDrive, "please create drive first", err) 39 | } 40 | } 41 | return GetRecycleBinResult{}, errors.New(ErrCodeUseCase, "fail to get drive", err) 42 | } 43 | 44 | folders, err := h.repo.GetFolderRepo().FindFolder( 45 | ctx, 46 | repository.FindFolderOptions{ 47 | DriveId: drive.Id(), 48 | States: []domain.State{domain.StateTrashedRoot}, 49 | }, 50 | ) 51 | 52 | files, err := h.repo.GetFileRepo().FindFile( 53 | ctx, 54 | repository.FindFileOptions{ 55 | DriveId: drive.Id(), 56 | States: []domain.State{domain.StateTrashedRoot}, 57 | }, 58 | ) 59 | 60 | return GetRecycleBinResult{ 61 | Children: ToChildren(folders, files), 62 | }, nil 63 | } 64 | 65 | type GetRecycleBinHandler decorator.Handler[GetRecycleBinArgs, GetRecycleBinResult] 66 | 67 | func NewGetRecycleBinHandler( 68 | repo repository.Repository, 69 | userService remote.UserService, 70 | logger *logrus.Entry, 71 | ) GetRecycleBinHandler { 72 | return decorator.WithLogging[GetRecycleBinArgs, GetRecycleBinResult]( 73 | getRecycleBinHandler{ 74 | repo: repo, 75 | userService: userService, 76 | }, 77 | logger, 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /backend/drive/usecases/start_download.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/remote" 7 | "github.com/axli-personal/drive/backend/drive/repository" 8 | "github.com/google/uuid" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type ( 13 | StartDownloadArgs struct { 14 | SessionId string 15 | FileId uuid.UUID 16 | } 17 | 18 | StartDownloadResult struct { 19 | FileName string 20 | FileHash string 21 | } 22 | ) 23 | 24 | type startDownloadHandler struct { 25 | userService remote.UserService 26 | driveRepo repository.DriveRepository 27 | fileRepo repository.FileRepository 28 | } 29 | 30 | func (handler startDownloadHandler) Handle(ctx context.Context, args StartDownloadArgs) (StartDownloadResult, error) { 31 | var driveId uuid.UUID 32 | 33 | if args.SessionId != "" { 34 | user, err := handler.userService.GetUser(ctx, args.SessionId) 35 | if err != nil { 36 | return StartDownloadResult{}, err 37 | } 38 | 39 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 40 | if err != nil { 41 | return StartDownloadResult{}, err 42 | } 43 | 44 | driveId = drive.Id() 45 | } 46 | 47 | file, err := handler.fileRepo.GetFile(ctx, args.FileId) 48 | if err != nil { 49 | return StartDownloadResult{}, err 50 | } 51 | 52 | err = file.CanRead(driveId) 53 | if err != nil { 54 | return StartDownloadResult{}, err 55 | } 56 | 57 | file.IncreaseDownloadCounts() 58 | 59 | err = handler.fileRepo.UpdateFile( 60 | ctx, 61 | file, 62 | repository.UpdateFileOptions{}, 63 | ) 64 | if err != nil { 65 | return StartDownloadResult{}, err 66 | } 67 | 68 | return StartDownloadResult{ 69 | FileName: file.Name(), 70 | FileHash: file.Hash(), 71 | }, nil 72 | } 73 | 74 | type StartDownloadHandler decorator.Handler[StartDownloadArgs, StartDownloadResult] 75 | 76 | func NewStartDownloadHandler( 77 | userService remote.UserService, 78 | driveRepo repository.DriveRepository, 79 | fileRepo repository.FileRepository, 80 | logger *logrus.Entry, 81 | ) StartDownloadHandler { 82 | return decorator.WithLogging[StartDownloadArgs, StartDownloadResult]( 83 | startDownloadHandler{ 84 | userService: userService, 85 | driveRepo: driveRepo, 86 | fileRepo: fileRepo, 87 | }, 88 | logger, 89 | ) 90 | } 91 | -------------------------------------------------------------------------------- /backend/drive/domain/drive.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "errors" 5 | "github.com/google/uuid" 6 | ) 7 | 8 | const ( 9 | KB = 1024 10 | MB = 1024 * KB 11 | GB = 1024 * MB 12 | ) 13 | 14 | var ( 15 | ErrExceedStorageUsage = errors.New("exceed storage usage") 16 | ) 17 | 18 | var ( 19 | StoragePlanFree = StoragePlan{name: "Free", maxBytes: 1 * GB} 20 | ) 21 | 22 | // How many bytes in the drive. 23 | type StorageUsage struct { 24 | bytes int 25 | } 26 | 27 | func NewStorageUsage(bytes int) (StorageUsage, error) { 28 | return StorageUsage{bytes: bytes}, nil 29 | } 30 | 31 | func (usage StorageUsage) Bytes() int { 32 | return usage.bytes 33 | } 34 | 35 | // The storage plan of the drive, used to control usage. 36 | type StoragePlan struct { 37 | name string 38 | maxBytes int 39 | } 40 | 41 | func NewStoragePlan(name string, maxBytes int) (StoragePlan, error) { 42 | return StoragePlan{name: name, maxBytes: maxBytes}, nil 43 | } 44 | 45 | func (plan StoragePlan) Name() string { 46 | return plan.name 47 | } 48 | 49 | func (plan StoragePlan) MaxBytes() int { 50 | return plan.maxBytes 51 | } 52 | 53 | type Drive struct { 54 | id uuid.UUID 55 | owner string 56 | usage StorageUsage 57 | plan StoragePlan 58 | } 59 | 60 | func NewDrive(owner string) (*Drive, error) { 61 | return &Drive{ 62 | id: uuid.New(), 63 | owner: owner, 64 | usage: StorageUsage{ 65 | bytes: 0, 66 | }, 67 | plan: StoragePlanFree, 68 | }, nil 69 | } 70 | 71 | func NewDriveFromRepository( 72 | id uuid.UUID, 73 | owner string, 74 | usage StorageUsage, 75 | plan StoragePlan, 76 | ) (*Drive, error) { 77 | return &Drive{ 78 | id: id, 79 | owner: owner, 80 | usage: usage, 81 | plan: plan, 82 | }, nil 83 | } 84 | 85 | func (drive *Drive) Id() uuid.UUID { 86 | return drive.id 87 | } 88 | 89 | func (drive *Drive) Owner() string { 90 | return drive.owner 91 | } 92 | 93 | func (drive *Drive) Usage() StorageUsage { 94 | return drive.usage 95 | } 96 | 97 | func (drive *Drive) Plan() StoragePlan { 98 | return drive.plan 99 | } 100 | 101 | func (drive *Drive) IncreaseUsage(bytes int) error { 102 | if drive.usage.bytes+bytes > drive.plan.maxBytes { 103 | return ErrExceedStorageUsage 104 | } 105 | 106 | drive.usage.bytes += bytes 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /backend/channel/usecases/find_message.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/axli-personal/drive/backend/channel/repository" 7 | "github.com/axli-personal/drive/backend/common/decorator" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ( 14 | FindMessageArgs struct { 15 | UserAccount string 16 | ChannelId uuid.UUID 17 | From int 18 | Limit int 19 | } 20 | 21 | FindMessageResult struct { 22 | Messages []*domain.Message 23 | } 24 | ) 25 | 26 | type findMessageHandler struct { 27 | channelRepo repository.ChannelRepository 28 | memberRepo repository.MemberRepository 29 | messageRepo repository.MessageRepository 30 | } 31 | 32 | func (h findMessageHandler) Handle(ctx context.Context, args FindMessageArgs) (FindMessageResult, error) { 33 | channel, err := h.channelRepo.GetChannel(ctx, args.ChannelId) 34 | if err != nil { 35 | return FindMessageResult{}, errors.New(ErrCodeChannelNotFound, "fail to find message", err) 36 | } 37 | 38 | if args.UserAccount != channel.OwnerAccount { 39 | member, err := h.memberRepo.FindMember(ctx, args.ChannelId, args.UserAccount) 40 | if err != nil { 41 | return FindMessageResult{}, errors.New(ErrCodeChannelNotFound, "fail to find message", err) 42 | } 43 | 44 | if !member.CanReadMessage() { 45 | return FindMessageResult{}, errors.New(ErrCodeUnauthorized, "fail to find message", err) 46 | } 47 | } 48 | 49 | messages, err := h.messageRepo.FindMessage( 50 | ctx, 51 | args.ChannelId, 52 | repository.Page{ 53 | From: args.From, 54 | Limit: args.Limit, 55 | }, 56 | ) 57 | if err != nil { 58 | return FindMessageResult{}, errors.New(ErrCodeInternal, "fail to find message", err) 59 | } 60 | 61 | return FindMessageResult{Messages: messages}, nil 62 | } 63 | 64 | type FindMessageHandler decorator.Handler[FindMessageArgs, FindMessageResult] 65 | 66 | func NewFindMessageHandler( 67 | channelRepo repository.ChannelRepository, 68 | memberRepo repository.MemberRepository, 69 | messageRepo repository.MessageRepository, 70 | logger *logrus.Entry, 71 | ) FindMessageHandler { 72 | return decorator.WithLogging[FindMessageArgs, FindMessageResult]( 73 | findMessageHandler{ 74 | channelRepo: channelRepo, 75 | memberRepo: memberRepo, 76 | messageRepo: messageRepo, 77 | }, 78 | logger, 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /backend/channel/usecases/send_message.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/axli-personal/drive/backend/channel/repository" 7 | "github.com/axli-personal/drive/backend/common/decorator" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ( 14 | SendMessageArgs struct { 15 | UserAccount string 16 | ChannelId uuid.UUID 17 | Text string 18 | } 19 | 20 | SendMessageResult struct { 21 | } 22 | ) 23 | 24 | type sendMessageHandler struct { 25 | channelRepo repository.ChannelRepository 26 | memberRepo repository.MemberRepository 27 | messageRepo repository.MessageRepository 28 | } 29 | 30 | func (h sendMessageHandler) Handle(ctx context.Context, args SendMessageArgs) (SendMessageResult, error) { 31 | channel, err := h.channelRepo.GetChannel(ctx, args.ChannelId) 32 | if err != nil { 33 | return SendMessageResult{}, errors.New(ErrCodeChannelNotFound, "fail to send file", err) 34 | } 35 | 36 | if args.UserAccount != channel.OwnerAccount { 37 | member, err := h.memberRepo.FindMember(ctx, args.ChannelId, args.UserAccount) 38 | if err != nil { 39 | return SendMessageResult{}, errors.New(ErrCodeChannelNotFound, "fail to send message", err) 40 | } 41 | 42 | if !member.CanSendMessage() { 43 | return SendMessageResult{}, errors.New(ErrCodeUnauthorized, "fail to send message", err) 44 | } 45 | } 46 | 47 | channelMessage, err := domain.NewChannelMessage(args.ChannelId, args.UserAccount, args.Text) 48 | if err != nil { 49 | return SendMessageResult{}, errors.New(ErrCodeInternal, "fail to send message", err) 50 | } 51 | 52 | err = h.messageRepo.SaveMessage(ctx, channelMessage) 53 | if err != nil { 54 | return SendMessageResult{}, errors.New(ErrCodeInternal, "fail to send message", err) 55 | } 56 | 57 | return SendMessageResult{}, nil 58 | } 59 | 60 | type SendMessageHandler decorator.Handler[SendMessageArgs, SendMessageResult] 61 | 62 | func NewSendMessageHandler( 63 | channelRepo repository.ChannelRepository, 64 | memberRepo repository.MemberRepository, 65 | messageRepo repository.MessageRepository, 66 | logger *logrus.Entry, 67 | ) SendMessageHandler { 68 | return decorator.WithLogging[SendMessageArgs, SendMessageResult]( 69 | sendMessageHandler{ 70 | channelRepo: channelRepo, 71 | memberRepo: memberRepo, 72 | messageRepo: messageRepo, 73 | }, 74 | logger, 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /backend/channel/usecases/send_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/channel/domain" 6 | "github.com/axli-personal/drive/backend/channel/repository" 7 | "github.com/axli-personal/drive/backend/common/decorator" 8 | "github.com/axli-personal/drive/backend/pkg/errors" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | "time" 12 | ) 13 | 14 | const ( 15 | ErrCodeUnauthorized = "Unauthorized" 16 | ) 17 | 18 | type ( 19 | SendFileArgs struct { 20 | UserAccount string 21 | ChannelId uuid.UUID 22 | FileId string 23 | } 24 | 25 | SendFileResult struct { 26 | } 27 | ) 28 | 29 | type sendFileHandler struct { 30 | channelRepo repository.ChannelRepository 31 | memberRepo repository.MemberRepository 32 | fileRepo repository.FileRepository 33 | } 34 | 35 | func (h sendFileHandler) Handle(ctx context.Context, args SendFileArgs) (SendFileResult, error) { 36 | channel, err := h.channelRepo.GetChannel(ctx, args.ChannelId) 37 | if err != nil { 38 | return SendFileResult{}, errors.New(ErrCodeChannelNotFound, "fail to send file", err) 39 | } 40 | 41 | if args.UserAccount != channel.OwnerAccount { 42 | member, err := h.memberRepo.FindMember(ctx, args.ChannelId, args.UserAccount) 43 | if err != nil { 44 | return SendFileResult{}, errors.New(ErrCodeChannelNotFound, "fail to send file", err) 45 | } 46 | 47 | if !member.CanSendFile() { 48 | return SendFileResult{}, errors.New(ErrCodeUnauthorized, "fail to send file", err) 49 | } 50 | } 51 | 52 | // TODO: name hash size from drive service. 53 | 54 | channelFile := &domain.File{ 55 | Id: uuid.New(), 56 | ChannelId: args.ChannelId, 57 | SenderAccount: args.UserAccount, 58 | Name: "", 59 | Hash: "", 60 | Size: 0, 61 | SendAt: time.Now(), 62 | } 63 | 64 | err = h.fileRepo.SaveFile(ctx, channelFile) 65 | if err != nil { 66 | return SendFileResult{}, errors.New(ErrCodeInternal, "fail to send file", err) 67 | } 68 | 69 | return SendFileResult{}, nil 70 | } 71 | 72 | type SendFileHandler decorator.Handler[SendFileArgs, SendFileResult] 73 | 74 | func NewSendFileHandler( 75 | memberRepo repository.MemberRepository, 76 | fileRepo repository.FileRepository, 77 | logger *logrus.Entry, 78 | ) SendFileHandler { 79 | return decorator.WithLogging[SendFileArgs, SendFileResult]( 80 | sendFileHandler{ 81 | memberRepo: memberRepo, 82 | fileRepo: fileRepo, 83 | }, 84 | logger, 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /frontend/public/icon/top-nav/article.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/storage/adapters/capacity_repository.go: -------------------------------------------------------------------------------- 1 | package adapters 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/axli-personal/drive/backend/storage/repository" 7 | "github.com/redis/go-redis/v9" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | var ( 13 | ErrNoRequestCapacity = errors.New("no request capacity") 14 | ) 15 | 16 | const ( 17 | KeyDiskCapacity = "DiskCapacity" 18 | KeyRequestCapacity = "RequestCapacity" 19 | ) 20 | 21 | type RedisCapacityRepository struct { 22 | client *redis.Client 23 | endpoint string 24 | directoryPath string 25 | requestPerSecond int 26 | } 27 | 28 | func NewRedisCapacityRepository(connectionString string, endpoint string, directoryPath string, requestPerSecond int) (repository.CapacityRepository, error) { 29 | options, err := redis.ParseURL(connectionString) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | client := redis.NewClient(options) 35 | 36 | err = client.Ping(context.Background()).Err() 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | repo := RedisCapacityRepository{ 42 | client: client, 43 | endpoint: endpoint, 44 | directoryPath: directoryPath, 45 | requestPerSecond: requestPerSecond, 46 | } 47 | 48 | go func() { 49 | requestCapacityTicker := time.NewTicker(1 * time.Second) 50 | 51 | for range requestCapacityTicker.C { 52 | repo.updateRequestCapacity(context.Background()) 53 | } 54 | }() 55 | 56 | go func() { 57 | diskCapacityTicker := time.NewTicker(5 * time.Second) 58 | 59 | for range diskCapacityTicker.C { 60 | repo.updateDiskCapacity(context.Background()) 61 | } 62 | }() 63 | 64 | return repo, nil 65 | } 66 | 67 | func (repo RedisCapacityRepository) DecreaseRequestCapacity(ctx context.Context) error { 68 | capacity, err := repo.client.Get(ctx, repo.endpoint+":"+KeyRequestCapacity).Int() 69 | if err != nil { 70 | return err 71 | } 72 | if capacity <= 0 { 73 | return ErrNoRequestCapacity 74 | } 75 | 76 | return repo.client.Decr(ctx, repo.endpoint+":"+KeyRequestCapacity).Err() 77 | } 78 | 79 | func (repo RedisCapacityRepository) updateDiskCapacity(ctx context.Context) error { 80 | stat := syscall.Statfs_t{} 81 | 82 | err := syscall.Statfs(repo.directoryPath, &stat) 83 | if err != nil { 84 | return err 85 | } 86 | 87 | return repo.client.Set(ctx, repo.endpoint+":"+KeyDiskCapacity, int64(stat.Bfree)*stat.Bsize, 0).Err() 88 | } 89 | 90 | func (repo RedisCapacityRepository) updateRequestCapacity(ctx context.Context) error { 91 | return repo.client.Set(ctx, repo.endpoint+":"+KeyRequestCapacity, repo.requestPerSecond, 0).Err() 92 | } 93 | -------------------------------------------------------------------------------- /backend/drive/usecases/get_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | "time" 12 | ) 13 | 14 | type ( 15 | GetFileArgs struct { 16 | FileId uuid.UUID 17 | SessionId string 18 | } 19 | 20 | GetFileResult struct { 21 | FileId uuid.UUID 22 | Parent domain.Parent 23 | Name string 24 | Shared bool 25 | LastChange time.Time 26 | Bytes int 27 | DownloadCounts int 28 | } 29 | ) 30 | 31 | type getFileHandler struct { 32 | userService remote.UserService 33 | driveRepo repository.DriveRepository 34 | fileRepo repository.FileRepository 35 | logger *logrus.Entry 36 | } 37 | 38 | func (handler getFileHandler) Handle(ctx context.Context, args GetFileArgs) (GetFileResult, error) { 39 | userDriveId := uuid.Nil 40 | 41 | if args.SessionId != "" { 42 | user, err := handler.userService.GetUser(ctx, args.SessionId) 43 | if err != nil { 44 | return GetFileResult{}, err 45 | } 46 | 47 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 48 | if err != nil { 49 | return GetFileResult{}, err 50 | } 51 | 52 | userDriveId = drive.Id() 53 | } 54 | 55 | file, err := handler.fileRepo.GetFile(ctx, args.FileId) 56 | if err != nil { 57 | return GetFileResult{}, err 58 | } 59 | 60 | err = file.CanRead(userDriveId) 61 | if err != nil { 62 | return GetFileResult{}, err 63 | } 64 | 65 | result := GetFileResult{ 66 | FileId: file.Id(), 67 | Name: file.Name(), 68 | Shared: file.State() == domain.StateShared, 69 | LastChange: file.LastChange(), 70 | Bytes: file.Size(), 71 | DownloadCounts: file.DownloadCounts(), 72 | } 73 | 74 | if file.CanReadParent(userDriveId) == nil { 75 | result.Parent = file.Parent() 76 | } 77 | 78 | return result, nil 79 | } 80 | 81 | type GetFileHandler decorator.Handler[GetFileArgs, GetFileResult] 82 | 83 | func NewGetFileHandler( 84 | userService remote.UserService, 85 | driveRepo repository.DriveRepository, 86 | fileRepo repository.FileRepository, 87 | logger *logrus.Entry, 88 | ) GetFileHandler { 89 | return decorator.WithLogging[GetFileArgs, GetFileResult]( 90 | getFileHandler{ 91 | userService: userService, 92 | driveRepo: driveRepo, 93 | fileRepo: fileRepo, 94 | logger: logger, 95 | }, 96 | logger, 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /frontend/src/views/identity/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 64 | 65 | 107 | -------------------------------------------------------------------------------- /backend/drive/usecases/move_file.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ( 14 | MoveFileArgs struct { 15 | SessionId string 16 | FileId uuid.UUID 17 | FileParent domain.Parent 18 | } 19 | 20 | MoveFileResult struct { 21 | } 22 | ) 23 | 24 | type moveFileHandler struct { 25 | userService remote.UserService 26 | driveRepo repository.DriveRepository 27 | folderRepo repository.FolderRepository 28 | fileRepo repository.FileRepository 29 | } 30 | 31 | func (handler moveFileHandler) Handle(ctx context.Context, args MoveFileArgs) (MoveFileResult, error) { 32 | user, err := handler.userService.GetUser(ctx, args.SessionId) 33 | if err != nil { 34 | return MoveFileResult{}, err 35 | } 36 | 37 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 38 | if err != nil { 39 | return MoveFileResult{}, err 40 | } 41 | 42 | file, err := handler.fileRepo.GetFile(ctx, args.FileId) 43 | if err != nil { 44 | return MoveFileResult{}, err 45 | } 46 | 47 | err = file.CanWrite(drive.Id()) 48 | if err != nil { 49 | return MoveFileResult{}, err 50 | } 51 | 52 | if args.FileParent.IsFolder() { 53 | parentFolder, err := handler.folderRepo.GetFolder(ctx, args.FileParent.FolderId()) 54 | if err != nil { 55 | // Move to folder that does not exist. 56 | return MoveFileResult{}, err 57 | } 58 | 59 | err = parentFolder.CanWrite(drive.Id()) 60 | if err != nil { 61 | // Cannot write to parent folder. 62 | return MoveFileResult{}, err 63 | } 64 | } 65 | 66 | err = file.ChangeParent(args.FileParent) 67 | if err != nil { 68 | return MoveFileResult{}, err 69 | } 70 | 71 | err = handler.fileRepo.UpdateFile( 72 | ctx, 73 | file, 74 | repository.UpdateFileOptions{}, 75 | ) 76 | if err != nil { 77 | return MoveFileResult{}, err 78 | } 79 | 80 | return MoveFileResult{}, err 81 | } 82 | 83 | type MoveFileHandler decorator.Handler[MoveFileArgs, MoveFileResult] 84 | 85 | func NewMoveFileHandler( 86 | userService remote.UserService, 87 | driveRepo repository.DriveRepository, 88 | folderRepo repository.FolderRepository, 89 | fileRepo repository.FileRepository, 90 | logger *logrus.Entry, 91 | ) MoveFileHandler { 92 | return decorator.WithLogging[MoveFileArgs, MoveFileResult]( 93 | moveFileHandler{ 94 | userService: userService, 95 | driveRepo: driveRepo, 96 | folderRepo: folderRepo, 97 | fileRepo: fileRepo, 98 | }, 99 | logger, 100 | ) 101 | } 102 | -------------------------------------------------------------------------------- /backend/drive/usecases/move_folder.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type ( 14 | MoveFolderArgs struct { 15 | SessionId string 16 | FolderId uuid.UUID 17 | Parent domain.Parent 18 | } 19 | 20 | MoveFolderResult struct { 21 | } 22 | ) 23 | 24 | type moveFolderHandler struct { 25 | userService remote.UserService 26 | driveRepo repository.DriveRepository 27 | folderRepo repository.FolderRepository 28 | } 29 | 30 | func (handler moveFolderHandler) Handle( 31 | ctx context.Context, 32 | args MoveFolderArgs, 33 | ) (MoveFolderResult, error) { 34 | user, err := handler.userService.GetUser(ctx, args.SessionId) 35 | if err != nil { 36 | return MoveFolderResult{}, err 37 | } 38 | 39 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 40 | if err != nil { 41 | return MoveFolderResult{}, err 42 | } 43 | 44 | folder, err := handler.folderRepo.GetFolder(ctx, args.FolderId) 45 | if err != nil { 46 | return MoveFolderResult{}, err 47 | } 48 | 49 | err = folder.CanWrite(drive.Id()) 50 | if err != nil { 51 | return MoveFolderResult{}, err 52 | } 53 | 54 | if args.Parent.IsFolder() { 55 | parentFolder, err := handler.folderRepo.GetFolder(ctx, args.Parent.FolderId()) 56 | if err != nil { 57 | // Move to folder that does not exist. 58 | return MoveFolderResult{}, err 59 | } 60 | 61 | err = parentFolder.CanWrite(drive.Id()) 62 | if err != nil { 63 | // Cannot write to parent folder. 64 | return MoveFolderResult{}, err 65 | } 66 | } 67 | 68 | err = folder.ChangeParent(args.Parent) 69 | if err != nil { 70 | return MoveFolderResult{}, err 71 | } 72 | 73 | // Only once transaction. 74 | err = handler.folderRepo.UpdateFolder( 75 | ctx, 76 | folder, 77 | repository.UpdateFolderOptions{ 78 | MustInSameState: true, 79 | }, 80 | ) 81 | if err != nil { 82 | return MoveFolderResult{}, err 83 | } 84 | 85 | return MoveFolderResult{}, err 86 | } 87 | 88 | type MoveFolderHandler decorator.Handler[MoveFolderArgs, MoveFolderResult] 89 | 90 | func NewMoveFolderHandler( 91 | userService remote.UserService, 92 | driveRepo repository.DriveRepository, 93 | folderRepo repository.FolderRepository, 94 | logger *logrus.Entry, 95 | ) MoveFolderHandler { 96 | return decorator.WithLogging[MoveFolderArgs, MoveFolderResult]( 97 | moveFolderHandler{ 98 | userService: userService, 99 | driveRepo: driveRepo, 100 | folderRepo: folderRepo, 101 | }, 102 | logger, 103 | ) 104 | } 105 | -------------------------------------------------------------------------------- /backend/drive/usecases/get_drive.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/axli-personal/drive/backend/pkg/errors" 10 | "github.com/google/uuid" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | var ( 15 | ErrCodeUseCase = "UseCase" 16 | ErrCodeNotCreateDrive = "NotCreateDrive" 17 | ) 18 | 19 | type ( 20 | GetDriveArgs struct { 21 | SessionId string 22 | } 23 | 24 | GetDriveResult struct { 25 | Id uuid.UUID 26 | Children Children 27 | Usage domain.StorageUsage 28 | Plan domain.StoragePlan 29 | } 30 | ) 31 | 32 | type getDriveHandler struct { 33 | userService remote.UserService 34 | driveRepo repository.DriveRepository 35 | folderRepo repository.FolderRepository 36 | fileRepo repository.FileRepository 37 | } 38 | 39 | func (handler getDriveHandler) Handle(ctx context.Context, args GetDriveArgs) (GetDriveResult, error) { 40 | user, err := handler.userService.GetUser(ctx, args.SessionId) 41 | if err != nil { 42 | return GetDriveResult{}, err 43 | } 44 | 45 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 46 | if err != nil { 47 | if err, ok := err.(*errors.Error); ok { 48 | if err.Code() == repository.ErrCodeNotFound { 49 | return GetDriveResult{}, errors.New(ErrCodeNotCreateDrive, "please create drive first", err) 50 | } 51 | } 52 | return GetDriveResult{}, errors.New(ErrCodeUseCase, "fail to get drive", err) 53 | } 54 | 55 | folders, err := handler.folderRepo.FindFolder( 56 | ctx, 57 | repository.FindFolderOptions{ 58 | Parent: domain.CreateDriveParent(), 59 | States: []domain.State{domain.StatePrivate, domain.StateShared}, 60 | }, 61 | ) 62 | 63 | files, err := handler.fileRepo.FindFile( 64 | ctx, 65 | repository.FindFileOptions{ 66 | Parent: domain.CreateDriveParent(), 67 | States: []domain.State{domain.StatePrivate, domain.StateShared}, 68 | }, 69 | ) 70 | 71 | return GetDriveResult{ 72 | Id: drive.Id(), 73 | Children: ToChildren(folders, files), 74 | Usage: drive.Usage(), 75 | Plan: drive.Plan(), 76 | }, nil 77 | } 78 | 79 | type GetDriveHandler decorator.Handler[GetDriveArgs, GetDriveResult] 80 | 81 | func NewGetDriveHandler( 82 | userService remote.UserService, 83 | driveRepo repository.DriveRepository, 84 | folderRepo repository.FolderRepository, 85 | fileRepo repository.FileRepository, 86 | logger *logrus.Entry, 87 | ) GetDriveHandler { 88 | return decorator.WithLogging[GetDriveArgs, GetDriveResult]( 89 | getDriveHandler{ 90 | userService: userService, 91 | driveRepo: driveRepo, 92 | folderRepo: folderRepo, 93 | fileRepo: fileRepo, 94 | }, 95 | logger, 96 | ) 97 | } 98 | -------------------------------------------------------------------------------- /backend/drive/repository/repository.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/drive/domain" 6 | "github.com/axli-personal/drive/backend/pkg/events" 7 | "github.com/google/uuid" 8 | ) 9 | 10 | const ( 11 | ErrCodeRepository = "Repository" 12 | ErrCodeNotFound = "NotFound" 13 | ErrCodeDuplicated = "Duplicated" 14 | ) 15 | 16 | type Repository interface { 17 | Transaction(fn func(repo Repository) error) error 18 | GetDriveRepo() DriveRepository 19 | GetFolderRepo() FolderRepository 20 | GetFileRepo() FileRepository 21 | } 22 | 23 | type ( 24 | DriveRepository interface { 25 | CreateDrive(ctx context.Context, drive *domain.Drive, options CreateDriveOptions) error 26 | GetDrive(ctx context.Context, id uuid.UUID) (*domain.Drive, error) 27 | GetDriveByOwner(ctx context.Context, owner string) (*domain.Drive, error) 28 | UpdateDrive(ctx context.Context, drive *domain.Drive) error 29 | } 30 | 31 | CreateDriveOptions struct { 32 | OnlyOneDrive bool 33 | } 34 | ) 35 | 36 | type ( 37 | FolderRepository interface { 38 | SaveFolder(ctx context.Context, folder *domain.Folder) error 39 | GetFolder(ctx context.Context, id uuid.UUID) (*domain.Folder, error) 40 | FindFolder(ctx context.Context, options FindFolderOptions) ([]*domain.Folder, error) 41 | UpdateFolder(ctx context.Context, folder *domain.Folder, options UpdateFolderOptions) error 42 | } 43 | 44 | FindFolderOptions struct { 45 | DriveId uuid.UUID 46 | Parent domain.Parent 47 | States []domain.State 48 | } 49 | 50 | UpdateFolderOptions struct { 51 | MustInSameState bool // Deprecated 52 | MustInState domain.State // Deprecated 53 | UpdateChildrenState bool 54 | } 55 | ) 56 | 57 | type ( 58 | FileRepository interface { 59 | SaveFile(ctx context.Context, file *domain.File) error 60 | GetFile(ctx context.Context, id uuid.UUID) (*domain.File, error) 61 | FindFile(ctx context.Context, options FindFileOptions) ([]*domain.File, error) 62 | UpdateFile(ctx context.Context, file *domain.File, options UpdateFileOptions) error 63 | DeleteFile(ctx context.Context, file *domain.File) error 64 | } 65 | 66 | FindFileOptions struct { 67 | DriveId uuid.UUID 68 | Parent domain.Parent 69 | Name string 70 | States []domain.State 71 | } 72 | 73 | UpdateFileOptions struct { 74 | MustInSameState bool 75 | MustInState domain.State 76 | IncreaseStorageUsage bool 77 | } 78 | ) 79 | 80 | type ( 81 | EventRepository interface { 82 | PublishFolderRemoved(ctx context.Context, event events.FolderRemoved) error 83 | PublishFileDeleted(ctx context.Context, event events.FileDeleted) error 84 | GetFileUploaded(ctx context.Context) (events.FileUploaded, error) 85 | GetFileDownloaded(ctx context.Context) (events.FileDownloaded, error) 86 | AckGetFileUploaded(ctx context.Context, id string) error 87 | AckFileDownloaded(ctx context.Context, id string) error 88 | } 89 | ) 90 | -------------------------------------------------------------------------------- /frontend/src/components/drive/CreateDrawer.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 85 | 86 | 91 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | mysql: 3 | image: mysql 4 | restart: always 5 | deploy: 6 | resources: 7 | limits: 8 | cpus: "2.00" 9 | environment: 10 | MYSQL_ROOT_PASSWORD: 1234qwer 11 | MYSQL_DATABASE: drive 12 | ports: 13 | - 3306:3306 # Expose for debug. 14 | healthcheck: 15 | test: "mysql -p$$MYSQL_ROOT_PASSWORD -e 'SHOW TABLES' $$MYSQL_DATABASE" 16 | interval: 5s 17 | timeout: 1s 18 | retries: 3 19 | networks: 20 | drive-net: 21 | redis: 22 | image: redis 23 | restart: always 24 | deploy: 25 | resources: 26 | limits: 27 | cpus: "1.00" 28 | ports: 29 | - 6379:6379 # Expose for debug. 30 | networks: 31 | drive-net: 32 | user: 33 | image: user 34 | build: 35 | context: backend/user 36 | dockerfile: Dockerfile 37 | restart: always 38 | deploy: 39 | resources: 40 | limits: 41 | cpus: "0.50" 42 | environment: 43 | MYSQL_CONNECTION_STRING: root:1234qwer@tcp(mysql)/drive?charset=utf8mb4&parseTime=True&loc=Local 44 | REDIS_CONNECTION_STRING: redis://redis:6379/0 45 | LOG_LEVEL: DEBUG 46 | depends_on: 47 | mysql: 48 | condition: service_healthy 49 | redis: 50 | condition: service_started 51 | ports: 52 | - 8080:8080 53 | networks: 54 | drive-net: 55 | drive: 56 | image: drive 57 | build: 58 | context: backend/drive 59 | dockerfile: Dockerfile 60 | restart: always 61 | deploy: 62 | resources: 63 | limits: 64 | cpus: "1.00" 65 | environment: 66 | MYSQL_CONNECTION_STRING: root:1234qwer@tcp(mysql)/drive?charset=utf8mb4&parseTime=True&loc=Local 67 | REDIS_CONNECTION_STRING: redis://redis:6379/0 68 | USER_SERVICE_ADDRESS: user:8081 69 | LOG_LEVEL: DEBUG 70 | depends_on: 71 | mysql: 72 | condition: service_healthy 73 | redis: 74 | condition: service_started 75 | ports: 76 | - 8081:8080 77 | networks: 78 | drive-net: 79 | storage: 80 | image: storage 81 | build: 82 | context: backend/storage 83 | dockerfile: Dockerfile 84 | restart: always 85 | deploy: 86 | resources: 87 | limits: 88 | cpus: "0.50" 89 | environment: 90 | ENDPOINT: http://localhost:8090 91 | DATA_DIRECTORY: /data 92 | REQUEST_PER_SECOND: 100 93 | DRIVE_SERVICE_ADDRESS: drive:8081 94 | REDIS_CONNECTION_STRING: redis://redis:6379/0 95 | LOG_LEVEL: DEBUG 96 | depends_on: 97 | mysql: 98 | condition: service_healthy 99 | redis: 100 | condition: service_started 101 | ports: 102 | - 8082:8080 103 | networks: 104 | drive-net: 105 | web: 106 | image: web 107 | build: 108 | context: frontend 109 | dockerfile: Dockerfile 110 | restart: always 111 | deploy: 112 | resources: 113 | limits: 114 | cpus: "0.50" 115 | ports: 116 | - 80:80 117 | networks: 118 | drive-net: 119 | 120 | networks: 121 | drive-net: 122 | -------------------------------------------------------------------------------- /backend/drive/usecases/get_folder.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/google/uuid" 10 | "github.com/sirupsen/logrus" 11 | "time" 12 | ) 13 | 14 | type ( 15 | GetFolderArgs struct { 16 | FolderId uuid.UUID 17 | SessionId string 18 | } 19 | 20 | GetFolderResult struct { 21 | FolderId uuid.UUID 22 | Parent domain.Parent 23 | Name string 24 | Shared bool 25 | LastChange time.Time 26 | Children Children 27 | } 28 | ) 29 | 30 | type getFolderHandler struct { 31 | userService remote.UserService 32 | driveRepo repository.DriveRepository 33 | folderRepo repository.FolderRepository 34 | fileRepo repository.FileRepository 35 | } 36 | 37 | func (handler getFolderHandler) Handle(ctx context.Context, args GetFolderArgs) (GetFolderResult, error) { 38 | userDriveId := uuid.Nil 39 | 40 | if args.SessionId != "" { 41 | user, err := handler.userService.GetUser(ctx, args.SessionId) 42 | if err != nil { 43 | return GetFolderResult{}, err 44 | } 45 | 46 | drive, err := handler.driveRepo.GetDriveByOwner(ctx, user.Account()) 47 | if err != nil { 48 | return GetFolderResult{}, err 49 | } 50 | 51 | userDriveId = drive.Id() 52 | } 53 | 54 | folder, err := handler.folderRepo.GetFolder(ctx, args.FolderId) 55 | if err != nil { 56 | return GetFolderResult{}, err 57 | } 58 | 59 | err = folder.CanRead(userDriveId) 60 | if err != nil { 61 | return GetFolderResult{}, err 62 | } 63 | 64 | result := GetFolderResult{ 65 | FolderId: folder.Id(), 66 | Name: folder.Name(), 67 | Shared: folder.State() == domain.StateShared, 68 | LastChange: folder.LastChange(), 69 | } 70 | 71 | self, err := domain.CreateFolderParent(args.FolderId) 72 | if err != nil { 73 | return result, err 74 | } 75 | 76 | folders, err := handler.folderRepo.FindFolder( 77 | ctx, 78 | repository.FindFolderOptions{ 79 | Parent: self, 80 | }, 81 | ) 82 | if err != nil { 83 | return result, err 84 | } 85 | 86 | files, err := handler.fileRepo.FindFile( 87 | ctx, 88 | repository.FindFileOptions{ 89 | Parent: self, 90 | }, 91 | ) 92 | if err != nil { 93 | return result, err 94 | } 95 | 96 | result.Children = ToChildren(folders, files) 97 | 98 | if folder.CanReadParent(userDriveId) == nil { 99 | result.Parent = folder.Parent() 100 | } 101 | 102 | return result, nil 103 | } 104 | 105 | type GetFolderHandler decorator.Handler[GetFolderArgs, GetFolderResult] 106 | 107 | func NewGetFolderHandler( 108 | userService remote.UserService, 109 | driveRepo repository.DriveRepository, 110 | folderRepo repository.FolderRepository, 111 | fileRepo repository.FileRepository, 112 | logger *logrus.Entry, 113 | ) GetFolderHandler { 114 | return decorator.WithLogging[GetFolderArgs, GetFolderResult]( 115 | getFolderHandler{ 116 | userService: userService, 117 | driveRepo: driveRepo, 118 | folderRepo: folderRepo, 119 | fileRepo: fileRepo, 120 | }, 121 | logger, 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /backend/drive/usecases/start_upload.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "context" 5 | "github.com/axli-personal/drive/backend/common/decorator" 6 | "github.com/axli-personal/drive/backend/drive/domain" 7 | "github.com/axli-personal/drive/backend/drive/remote" 8 | "github.com/axli-personal/drive/backend/drive/repository" 9 | "github.com/axli-personal/drive/backend/pkg/errors" 10 | "github.com/google/uuid" 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | const ( 15 | ErrCodeFileNameExist = "FileNameExist" 16 | ) 17 | 18 | type ( 19 | StartUploadArgs struct { 20 | SessionId string 21 | FileParent domain.Parent 22 | FileName string 23 | FileHash string 24 | FileSize int 25 | } 26 | 27 | StartUploadResult struct { 28 | FileId uuid.UUID 29 | } 30 | ) 31 | 32 | type startUploadHandler struct { 33 | repo repository.Repository 34 | userService remote.UserService 35 | } 36 | 37 | func (h startUploadHandler) Handle(ctx context.Context, args StartUploadArgs) (result StartUploadResult, err error) { 38 | user, err := h.userService.GetUser(ctx, args.SessionId) 39 | if err != nil { 40 | return result, err 41 | } 42 | 43 | err = h.repo.Transaction(func(repo repository.Repository) error { 44 | drive, err := repo.GetDriveRepo().GetDriveByOwner(ctx, user.Account()) 45 | if err != nil { 46 | return err 47 | } 48 | 49 | files, err := repo.GetFileRepo().FindFile( 50 | ctx, 51 | repository.FindFileOptions{ 52 | DriveId: drive.Id(), 53 | Parent: args.FileParent, 54 | Name: args.FileName, 55 | }, 56 | ) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | if len(files) > 0 { 62 | if files[0].State() != domain.StateLocked { 63 | return errors.New(ErrCodeFileNameExist, "duplicated file", nil) 64 | } 65 | 66 | result.FileId = files[0].Id() 67 | 68 | err = drive.IncreaseUsage(args.FileSize - files[0].Size()) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | files[0].SetHash(args.FileHash) 74 | files[0].SetSize(args.FileSize) 75 | 76 | err = repo.GetFileRepo().UpdateFile(ctx, files[0], repository.UpdateFileOptions{}) 77 | if err != nil { 78 | return err 79 | } 80 | } else { 81 | file, err := domain.NewFile(drive.Id(), args.FileParent, args.FileName, args.FileSize, args.FileHash) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | result.FileId = file.Id() 87 | 88 | err = drive.IncreaseUsage(args.FileSize) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | err = repo.GetFileRepo().SaveFile(ctx, file) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | 99 | err = repo.GetDriveRepo().UpdateDrive(ctx, drive) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | return nil 105 | }) 106 | 107 | return result, err 108 | } 109 | 110 | type StartUploadHandler decorator.Handler[StartUploadArgs, StartUploadResult] 111 | 112 | func NewStartUploadHandler( 113 | repo repository.Repository, 114 | userService remote.UserService, 115 | logger *logrus.Entry, 116 | ) StartUploadHandler { 117 | return decorator.WithLogging[StartUploadArgs, StartUploadResult]( 118 | startUploadHandler{ 119 | repo: repo, 120 | userService: userService, 121 | }, 122 | logger, 123 | ) 124 | } 125 | -------------------------------------------------------------------------------- /backend/pkg/types/drive.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // RPC 8 | 9 | type StartDownloadRequest struct { 10 | SessionId string 11 | FileId string 12 | } 13 | 14 | type StartDownloadResponse struct { 15 | FileName string 16 | FileHash string 17 | } 18 | 19 | type StartUploadRequest struct { 20 | SessionId string 21 | FileParent string 22 | FileName string 23 | FileHash string 24 | FileSize int 25 | } 26 | 27 | type StartUploadResponse struct { 28 | FileId string 29 | } 30 | 31 | type FinishUploadRequest struct { 32 | FileId string 33 | } 34 | 35 | type FinishUploadResponse struct { 36 | } 37 | 38 | // HTTP 39 | 40 | type CreateFileRequest struct { 41 | Parent string `json:"parent"` 42 | FileName string `json:"fileName"` 43 | } 44 | 45 | type CreateFileResponse struct { 46 | FileId string `json:"fileId"` 47 | FileName string `json:"fileName"` 48 | LastChange time.Time `json:"lastChange"` 49 | StorageEndpoint string `json:"storageEndpoint"` 50 | } 51 | 52 | type CreateFolderRequest struct { 53 | Parent string `json:"parent"` 54 | FolderName string `json:"folderName"` 55 | } 56 | 57 | type GetFileRequest struct { 58 | FileId string `params:"fileId"` 59 | } 60 | 61 | type GetFileResponse struct { 62 | FileId string `json:"fileId"` 63 | Parent string `json:"parent"` 64 | Name string `json:"name"` 65 | Shared bool `json:"shared"` 66 | LastChange time.Time `json:"lastChange"` 67 | Bytes int `json:"bytes"` 68 | DownloadCounts int `json:"downloadCounts"` 69 | } 70 | 71 | type GetFolderRequest struct { 72 | FolderId string `params:"folderId"` 73 | } 74 | 75 | type GetFolderResponse struct { 76 | FolderId string `json:"folderId"` 77 | Parent string `json:"parent"` 78 | Name string `json:"name"` 79 | Shared bool `json:"shared"` 80 | LastChange time.Time `json:"lastChange"` 81 | Children Children `json:"children"` 82 | } 83 | 84 | type ShareFileRequest struct { 85 | FileId string `params:"fileId"` 86 | } 87 | 88 | type ShareFolderRequest struct { 89 | FolderId string `params:"folderId"` 90 | } 91 | 92 | type RemoveFileRequest struct { 93 | FileId string `params:"fileId"` 94 | } 95 | 96 | type RemoveFolderRequest struct { 97 | FolderId string `params:"folderId"` 98 | } 99 | 100 | type RestoreFileRequest struct { 101 | FileId string `params:"fileId"` 102 | } 103 | 104 | type RestoreFolderRequest struct { 105 | FolderId string `params:"folderId"` 106 | } 107 | 108 | type Children struct { 109 | Folders []FolderLink `json:"folders"` 110 | Files []FileLink `json:"files"` 111 | } 112 | 113 | type FolderLink struct { 114 | Id string `json:"id"` 115 | Name string `json:"name"` 116 | } 117 | 118 | type FileLink struct { 119 | Id string `json:"id"` 120 | Name string `json:"name"` 121 | Bytes int `json:"bytes"` 122 | } 123 | 124 | type GetDriveResponse struct { 125 | DriveId string `json:"driveId"` 126 | Children Children `json:"children"` 127 | PlanName string `json:"planName"` 128 | UsedBytes int `json:"usedBytes"` 129 | MaxBytes int `json:"maxBytes"` 130 | } 131 | 132 | type GetRecycleBinResponse struct { 133 | Children Children `json:"children"` 134 | } 135 | -------------------------------------------------------------------------------- /frontend/src/views/drive/RecycleBin.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 94 | 95 | 129 | --------------------------------------------------------------------------------