├── 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 |
2 | 正在跳转...
3 |
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 |
2 | 正在为您创建一块免费的硬盘...
3 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
34 |
--------------------------------------------------------------------------------
/frontend/src/components/ui/LabelButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ label }}
4 |
5 |
6 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
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 |
2 |
3 |
4 | 云端硬盘
5 | 回收站
6 |
7 |
8 | 注册
9 | 登录
10 |
11 |
12 |
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 |
2 |
3 |
4 | 登陆
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | 登陆
13 |
14 |
15 |
16 |
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 |
2 |
3 | {{ label }}
4 |
10 |
11 |
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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ label }}
9 |
10 |
11 |
12 |
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 |
2 |
3 |
4 | 注册
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 注册
19 |
20 |
21 |
22 |
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 |
2 |
3 |
4 |
5 | 搜索文章
6 |
7 |
8 |
9 |
10 |
14 |
15 |
16 |
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 |
2 |
3 |
推荐文章
4 |
5 |
6 |
7 |
8 |
9 |
10 |
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 |
2 |
3 |
欢迎来到控制面板, {{ account }}.
4 |
5 |
6 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 |
4 | 新建文件夹
5 | 上传文件
6 |
7 |
30 |
31 |
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 |
2 |
3 |
9 |
10 |
30 |
31 |
32 |
33 |
94 |
95 |
129 |
--------------------------------------------------------------------------------