├── .github └── FUNDING.yml ├── .gitignore ├── CONTRIBUTING.md ├── Discovery.md ├── LICENSE ├── README.md ├── Spacefile ├── actions.go ├── api.go ├── cron.go ├── deta ├── base.go ├── deta.go ├── drive.go ├── https.go ├── query.go ├── updater.go └── utils.go ├── go.mod ├── go.sum ├── main.go ├── manifest.json ├── previews ├── desktop.png └── mobile.png ├── static ├── assets │ ├── app_icon.png │ └── icon.png ├── pages │ ├── app.html │ └── shared.html ├── scripts │ ├── app.js │ └── shared.js └── styles │ └── app.css ├── utils.go └── worker.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [jnsougata] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .space 4 | .venvs -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /Discovery.md: -------------------------------------------------------------------------------- 1 | --- 2 | app_name: "Filebox" 3 | title: "Filebox" 4 | tagline: "Fastest File Storage (Supports Large File)" 5 | git: "https://github.com/jnsougata/filebox" 6 | theme_color: "#0561da" 7 | --- 8 | 9 | Upload & organize files in your Personal Space Drive 10 | 11 | # Features 12 | - Simple and clean UI 13 | - Multiple file upload 14 | - Upload file upto 10GB 15 | - Storage usage indicator 16 | - Easy file download on demand 17 | - Embeddable file urls (up to 4MB) 18 | - Publicly shareable urls of any file and folder 19 | - Preview for common file types 20 | - Cross instance file sharing 21 | - Upload and store folders ( ⚠ limited browser support ) 22 | - Zipped folder or multiple file download support 23 | 24 | ### 🍻 EOL 25 | Thanks for using Filebox. This will be replaced by a better version soon. Don't worry, you will always have an option for data migration. Stay tuned. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Sougata Jana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | # Filebox 5 | A free Google Drive alternative over **AWS Lambda** 6 | 7 | # Features 8 | - **UI:** Modern design, intuitive navigation 9 | - **File Upload:** Drag & drop, direct & chunked uploads 10 | - **Folder Upload:** Upload and store folders (⚠) 11 | - **Storage:** Detailed breakdown, storage usage indicator 12 | - **Download:** Choose format, progress indicator 13 | - **File Embed:** Secure embedding (size limit: 4MB) 14 | - **Sharing:** Publicly shareable file & folder urls 15 | - **Cross-Instance Sharing:** Share files across instances 16 | - **Zipped Downloads:** Granular selection, background compression 17 | - **Preview:** Preview a wider range of documents, images, and multimedia files 18 | 19 | ## Previews 20 | - **[Mobile](/previews/mobile.png)** 21 | - **[Desktop](/previews/desktop.png)** 22 | 23 | ## Installation 24 | [Install on Deta Space](https://deta.space/discovery/@gyrooo/filebox) 25 | 26 | ## Credits 27 | 🚀 **[Deta Space](https://deta.space)** 28 | 29 | 🎨 **[Box icons created by srip - Flaticon](https://www.flaticon.com/free-icons/box)** 30 | -------------------------------------------------------------------------------- /Spacefile: -------------------------------------------------------------------------------- 1 | v: 0 2 | icon: ./static/assets/app_icon.png 3 | micros: 4 | - name: backend 5 | src: . 6 | engine: custom 7 | primary: true 8 | provide_actions: true 9 | commands: 10 | - go get 11 | - go build . 12 | include: 13 | - backend 14 | - static/ 15 | - manifest.json 16 | - worker.js 17 | run: ./backend 18 | dev: go run . 19 | actions: 20 | - id: "cleanup" 21 | name: "cleanup" 22 | description: "Cleans up orphaned files" 23 | trigger: "schedule" 24 | default_interval: "0/15 * * * *" 25 | public_routes: 26 | - "/manifest.json" 27 | - "/worker.js" 28 | - "/static/*" 29 | - "/shared/*" 30 | - "/embed/*" 31 | - "/api/metadata/*" 32 | - "/api/download/*" 33 | - "/api/accept" 34 | - "/api/query" -------------------------------------------------------------------------------- /actions.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type Input struct { 11 | Name string `json:"name"` 12 | Type string `json:"type"` 13 | } 14 | 15 | type SpaceAppAction struct { 16 | Name string `json:"name"` 17 | Title string `json:"title"` 18 | Path string `json:"path"` 19 | Input []Input `json:"input"` 20 | Handler func(c *gin.Context) `json:"-"` 21 | } 22 | 23 | var Save = SpaceAppAction{ 24 | Name: "save", 25 | Title: "Save", 26 | Path: "/actions/save", 27 | Input: []Input{ 28 | { 29 | Name: "name", 30 | Type: "string"}, 31 | { 32 | Name: "content", 33 | Type: "string", 34 | }, 35 | }, 36 | Handler: func(c *gin.Context) { 37 | var data map[string]interface{} 38 | c.BindJSON(&data) 39 | name := data["name"].(string) 40 | content := []byte(data["content"].(string)) 41 | key := randomHex(32) 42 | record := map[string]interface{}{ 43 | "key": key, 44 | "name": name, 45 | "date": time.Now().Format("2006-01-02T15:04:05.000Z"), 46 | "parent": nil, 47 | "size": len(content), 48 | "mime": "text/plain", 49 | "hash": key, 50 | } 51 | base.Put(record) 52 | drive.Put(FileToDriveSavedName(record), content) 53 | c.String(200, fmt.Sprintf("Saved %s successfully", name)) 54 | }, 55 | } 56 | 57 | func AppActions(c *gin.Context) { 58 | c.JSON(200, map[string]interface{}{ 59 | "actions": []SpaceAppAction{Save}, 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "backend/deta" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "regexp" 11 | "strconv" 12 | "strings" 13 | 14 | "github.com/gin-gonic/gin" 15 | ) 16 | 17 | var detaProjectKey = os.Getenv("DETA_PROJECT_KEY") 18 | var connection = deta.New(detaProjectKey) 19 | var drive = connection.Drive("filebox") 20 | var base = connection.Base("filebox_metadata") 21 | var metadata = connection.Base("metadata") 22 | 23 | func Ping(c *gin.Context) { 24 | c.String(http.StatusOK, "pong") 25 | } 26 | 27 | func SharedPage(c *gin.Context) { 28 | hash := c.Param("hash") 29 | metadata := base.Get(hash).JSON() 30 | _, ok := metadata["hash"].(string) 31 | if !ok { 32 | c.String(http.StatusNotFound, "Not Found") 33 | return 34 | } 35 | access, _ := metadata["access"].(string) 36 | if access == "private" { 37 | c.String(http.StatusForbidden, "Forbidden") 38 | return 39 | } 40 | c.File("./static/pages/shared.html") 41 | } 42 | 43 | func ProjectKey(c *gin.Context) { 44 | c.JSON(http.StatusOK, map[string]interface{}{"key": detaProjectKey}) 45 | } 46 | 47 | func MicroId(c *gin.Context) { 48 | pattern := regexp.MustCompile(`^(.*?)\.`) 49 | matches := pattern.FindStringSubmatch(os.Getenv("DETA_SPACE_APP_HOSTNAME")) 50 | var id string 51 | if len(matches) < 2 { 52 | id = "unknown" 53 | } else { 54 | id = matches[1] 55 | } 56 | c.JSON(http.StatusOK, map[string]interface{}{"id": id}) 57 | } 58 | 59 | func Metadata(c *gin.Context) { 60 | switch c.Request.Method { 61 | 62 | case "POST": 63 | var metadata map[string]interface{} 64 | c.BindJSON(&metadata) 65 | metadata["key"] = metadata["hash"] 66 | _, isFolder := metadata["type"] 67 | if isFolder { 68 | q := deta.NewQuery() 69 | q.Equals("parent", metadata["parent"]) 70 | q.Equals("name", metadata["name"].(string)) 71 | resp := base.FetchUntilEnd(q).ArrayJSON() 72 | if len(resp) > 0 { 73 | c.JSON(http.StatusConflict, nil) 74 | return 75 | } 76 | } 77 | resp := base.Put(metadata) 78 | c.JSON(resp.StatusCode, resp.JSON()) 79 | 80 | case "PUT": 81 | var metadata map[string]interface{} 82 | c.BindJSON(&metadata) 83 | metadata["key"] = metadata["hash"] 84 | resp := base.Put(metadata) 85 | c.JSON(resp.StatusCode, resp.JSON()) 86 | 87 | case "PATCH": 88 | var metadata map[string]interface{} 89 | c.BindJSON(&metadata) 90 | updater := deta.NewUpdater(metadata["hash"].(string)) 91 | delete(metadata, "hash") 92 | for k, v := range metadata { 93 | updater.Set(k, v) 94 | } 95 | resp := base.Update(updater) 96 | c.JSON(resp.StatusCode, resp.JSON()) 97 | 98 | case "DELETE": 99 | metadata, _ := io.ReadAll(c.Request.Body) 100 | var file map[string]interface{} 101 | _ = json.Unmarshal(metadata, &file) 102 | _, isFolder := file["type"] 103 | if isFolder { 104 | _ = base.Delete(file["hash"].(string)) 105 | c.JSON(http.StatusOK, nil) 106 | return 107 | } 108 | hash := file["hash"].(string) 109 | _ = base.Delete(hash) 110 | _ = drive.Delete(FileToDriveSavedName(file)) 111 | c.JSON(http.StatusOK, nil) 112 | 113 | default: 114 | c.JSON(http.StatusMethodNotAllowed, nil) 115 | } 116 | } 117 | 118 | func EmbedFile(c *gin.Context) { 119 | hash := c.Param("hash") 120 | resp := base.Get(hash) 121 | metadata := resp.JSON() 122 | access, ok := metadata["access"] 123 | if ok && access.(string) == "private" { 124 | c.String(http.StatusForbidden, "Unauthorized") 125 | } 126 | isDeleted, ok := metadata["deleted"] 127 | if ok && isDeleted.(bool) { 128 | c.String(http.StatusNotFound, "File not found") 129 | return 130 | } 131 | if metadata["size"].(float64) > 5*1024*1024 { 132 | c.String(http.StatusBadGateway, "File too large") 133 | return 134 | } 135 | fileName := metadata["name"].(string) 136 | mime := metadata["mime"].(string) 137 | c.Header("Content-Disposition", fmt.Sprintf("inline; filename=%s", fileName)) 138 | streamingResp := drive.Get(FileToDriveSavedName(metadata)) 139 | c.Data(http.StatusOK, mime, streamingResp.Body) 140 | } 141 | 142 | func DownloadFile(c *gin.Context) { 143 | hash := c.Param("hash") 144 | part := c.Param("part") 145 | recipient := c.Param("recipient") 146 | resp := base.Get(hash) 147 | metadata := resp.JSON() 148 | if recipient == "na" { 149 | access, ok := metadata["access"] 150 | if ok && access.(string) == "private" { 151 | c.String(http.StatusForbidden, "Unauthorized") 152 | return 153 | } 154 | } else { 155 | recipients, ok := metadata["recipients"] 156 | if !ok { 157 | c.String(http.StatusForbidden, "Unauthorized") 158 | return 159 | } 160 | recipientsList := recipients.([]interface{}) 161 | found := false 162 | for _, rec := range recipientsList { 163 | if rec.(string) == recipient { 164 | found = true 165 | break 166 | } 167 | } 168 | if !found { 169 | c.String(http.StatusForbidden, "Unauthorized") 170 | return 171 | } 172 | } 173 | skip, _ := strconv.Atoi(part) 174 | ProjectId := strings.Split(detaProjectKey, "_")[0] 175 | url := fmt.Sprintf( 176 | "https://drive.deta.sh/v1/%s/filebox/files/download?name=%s", 177 | ProjectId, 178 | FileToDriveSavedName(metadata), 179 | ) 180 | req, _ := http.NewRequest("GET", url, nil) 181 | req.Header.Set("X-API-Key", detaProjectKey) 182 | if (skip+1)*4*1024*1024 > int(metadata["size"].(float64)) { 183 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-", skip*4*1024*1024)) 184 | } else { 185 | req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", skip*4*1024*1024, (skip+1)*4*1024*1024-1)) 186 | } 187 | client := &http.Client{} 188 | fResp, _ := client.Do(req) 189 | data, _ := io.ReadAll(fResp.Body) 190 | c.Data(http.StatusOK, "application/octet-stream", data) 191 | } 192 | 193 | func SharedMeta(c *gin.Context) { 194 | hash := c.Param("hash") 195 | resp := base.Get(hash) 196 | data := resp.JSON() 197 | delete(data, "key") 198 | delete(data, "deleted") 199 | delete(data, "recipients") 200 | delete(data, "project_id") 201 | c.JSON(http.StatusOK, data) 202 | } 203 | 204 | func Query(c *gin.Context) { 205 | var data map[string]interface{} 206 | c.BindJSON(&data) 207 | ok, _ := data["__union"].(bool) 208 | q := deta.NewQuery() 209 | if ok { 210 | raw := data["__queries"].([]interface{}) 211 | var queries []map[string]interface{} 212 | for _, item := range raw { 213 | queries = append(queries, item.(map[string]interface{})) 214 | } 215 | q.Value = append(q.Value, queries...) 216 | q.Value = q.Value[1:] 217 | delete(data, "__union") 218 | delete(data, "__queries") 219 | } 220 | for k, v := range data { 221 | q.Equals(k, v) 222 | } 223 | resp := base.FetchUntilEnd(q).ArrayJSON() 224 | c.JSON(http.StatusOK, resp) 225 | } 226 | 227 | func Consumption(c *gin.Context) { 228 | q := deta.NewQuery() 229 | q.NotEquals("type", "folder") 230 | q.NotEquals("shared", true) 231 | resp := base.FetchUntilEnd(q) 232 | consumption := 0 233 | files := resp.ArrayJSON() 234 | for _, file := range files { 235 | size, ok := file["size"] 236 | if ok { 237 | consumption += int(size.(float64)) 238 | } 239 | } 240 | c.JSON(http.StatusOK, map[string]interface{}{"size": consumption}) 241 | } 242 | 243 | func FolderChildrenCount(c *gin.Context) { 244 | var targets []map[string]interface{} 245 | c.BindJSON(&targets) 246 | counterMap := map[string]interface{}{} 247 | for _, target := range targets { 248 | counterMap[FolderToAsParentPath(target)] = map[string]interface{}{ 249 | "hash": target["hash"], "count": 0, 250 | } 251 | } 252 | q := deta.NewQuery() 253 | q.Value = []map[string]interface{}{} 254 | var queries []deta.Query 255 | for parentPath := range counterMap { 256 | nq := deta.NewQuery() 257 | nq.Equals("parent", parentPath) 258 | nq.NotEquals("deleted", true) 259 | queries = append(queries, *nq) 260 | } 261 | q.Union(queries...) 262 | resp := base.FetchUntilEnd(q) 263 | items := resp.ArrayJSON() 264 | for _, item := range items { 265 | path := item["parent"].(string) 266 | ctxFolder, ok := counterMap[path] 267 | if ok { 268 | ctxFolderMap := ctxFolder.(map[string]interface{}) 269 | ctxFolderMap["count"] = ctxFolderMap["count"].(int) + 1 270 | } 271 | } 272 | var counts []interface{} 273 | for _, v := range counterMap { 274 | counts = append(counts, v) 275 | } 276 | c.JSON(resp.StatusCode, counts) 277 | } 278 | 279 | func FileBulkOps(c *gin.Context) { 280 | switch c.Request.Method { 281 | case "DELETE": 282 | var body []map[string]interface{} 283 | c.BindJSON(&body) 284 | var hashes []string 285 | var driveNames []string 286 | for _, item := range body { 287 | hashes = append(hashes, item["hash"].(string)) 288 | driveNames = append(driveNames, FileToDriveSavedName(item)) 289 | } 290 | ch := make(chan bool, len(hashes)) 291 | for _, hash := range hashes { 292 | go func(hash string) { 293 | _ = base.Delete(hash) 294 | ch <- true 295 | }(hash) 296 | } 297 | for i := 0; i < len(hashes); i++ { 298 | <-ch 299 | } 300 | _ = drive.Delete(driveNames...) 301 | c.String(http.StatusOK, "OK") 302 | return 303 | 304 | case "PATCH": 305 | var metadatas []map[string]interface{} 306 | c.BindJSON(&metadatas) 307 | var files []interface{} 308 | for _, metadata := range metadatas { 309 | metadata["key"] = metadata["hash"] 310 | files = append(files, metadata) 311 | } 312 | _ = base.Put(files...) 313 | c.String(http.StatusOK, "OK") 314 | return 315 | 316 | default: 317 | c.String(http.StatusMethodNotAllowed, "Method not allowed") 318 | return 319 | } 320 | } 321 | 322 | func DownloadFileExtern(c *gin.Context) { 323 | hash := c.Param("hash") 324 | skip := c.Param("part") 325 | owner := c.Param("owner") 326 | recipient := c.Param("recipient") 327 | req, err := http.NewRequest( 328 | "GET", 329 | fmt.Sprintf("https://%s.deta.app/api/download/%s/%s/%s", owner, recipient, hash, skip), 330 | nil) 331 | if err != nil { 332 | c.String(http.StatusInternalServerError, "Internal Server Error") 333 | return 334 | } 335 | client := &http.Client{} 336 | resp, err := client.Do(req) 337 | if err != nil { 338 | c.String(http.StatusInternalServerError, "Internal Server Error") 339 | return 340 | } 341 | data, _ := io.ReadAll(resp.Body) 342 | c.Data(http.StatusOK, "application/octet-stream", data) 343 | } 344 | 345 | func PushFileMeta(c *gin.Context) { 346 | targetId := c.Param("id") 347 | resp, _ := http.Post( 348 | fmt.Sprintf("https://filebox-%s.deta.app/api/accept", targetId), 349 | "application/json", 350 | c.Request.Body, 351 | ) 352 | c.JSON(resp.StatusCode, nil) 353 | } 354 | 355 | func AcceptFileMeta(c *gin.Context) { 356 | var metadata map[string]interface{} 357 | c.BindJSON(&metadata) 358 | metadata["key"] = metadata["hash"] 359 | resp := base.Put(metadata) 360 | c.JSON(resp.StatusCode, nil) 361 | } 362 | 363 | func SanitizeFiles(c *gin.Context) { 364 | files := base.FetchUntilEnd(deta.NewQuery()).ArrayJSON() 365 | var sanitized []map[string]interface{} 366 | for _, file := range files { 367 | _, ok := file["parent"] 368 | if !ok { 369 | file["parent"] = nil 370 | delete(file, "project_id") 371 | sanitized = append(sanitized, file) 372 | } 373 | } 374 | var batches [][]map[string]interface{} 375 | for i := 0; i < len(sanitized); i += 25 { 376 | end := i + 25 377 | if end > len(sanitized) { 378 | end = len(sanitized) 379 | } 380 | batches = append(batches, sanitized[i:end]) 381 | } 382 | var success = 0 383 | for _, batch := range batches { 384 | var metadatas []interface{} 385 | for _, metadata := range batch { 386 | metadata["key"] = metadata["hash"] 387 | metadatas = append(metadatas, metadata) 388 | } 389 | resp := base.Put(metadatas...) 390 | if resp.StatusCode == http.StatusMultiStatus { 391 | success += len(batch) 392 | } 393 | } 394 | c.JSON(http.StatusOK, map[string]interface{}{"sanitized": success}) 395 | } 396 | 397 | func MigrateV2(c *gin.Context) { 398 | var file FileV1 399 | c.BindJSON(&file) 400 | fileV2 := FileV2{ 401 | Key: file.Key, 402 | Name: file.Name, 403 | Color: file.Color, 404 | Deleted: file.Deleted, 405 | Size: file.Size, 406 | Type: file.Mime, 407 | Public: file.Access == "public" || file.Access == "", 408 | Folder: file.Type == "folder", 409 | Owner: "", 410 | Tag: []string{}, 411 | Partial: false, 412 | UploadedUpTo: file.Size, 413 | AccessTokens: []AccessToken{}, 414 | NameLowercase: strings.ToLower(file.Name), 415 | CreatedAt: file.Date, 416 | Path: buildPathV2FromV1(file.Parent), 417 | } 418 | resp := metadata.Put(fileV2) 419 | c.String(resp.StatusCode, "OK") 420 | } 421 | -------------------------------------------------------------------------------- /cron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func Job(c *gin.Context) { 10 | data := drive.Files("", 0, "").JSON() 11 | names := data["names"].([]interface{}) 12 | hashes := map[string]string{} 13 | for _, name := range names { 14 | fragments := strings.Split(name.(string), ".") 15 | if len(fragments) > 1 { 16 | hashes[fragments[0]] = name.(string) 17 | } else { 18 | hashes[name.(string)] = name.(string) 19 | } 20 | } 21 | var orphanNames []string 22 | for k, v := range hashes { 23 | resp := base.Get(k).JSON() 24 | _, ok := resp["hash"] 25 | if !ok { 26 | orphanNames = append(orphanNames, v) 27 | } 28 | } 29 | r := drive.Delete(orphanNames...) 30 | c.JSON(r.StatusCode, r.JSON()) 31 | } 32 | -------------------------------------------------------------------------------- /deta/base.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const BaseHost = "https://database.deta.sh/v1" 9 | 10 | type Base struct { 11 | Name string 12 | service *service 13 | } 14 | 15 | func (b *Base) Put(records ...any) *Response { 16 | if len(records) > 25 { 17 | records = records[:25] 18 | } 19 | var items []interface{} 20 | items = append(items, records...) 21 | resp, err := Request(Config{ 22 | Prefix: BaseHost, 23 | Body: map[string]interface{}{"items": items}, 24 | Method: "PUT", 25 | Path: fmt.Sprintf("/%s/%s/items", b.service.projectId, b.Name), 26 | AuthToken: b.service.key, 27 | }) 28 | return NewResponse(resp, err, 207) 29 | } 30 | 31 | func (b *Base) Get(key string) *Response { 32 | resp, err := Request(Config{ 33 | Prefix: BaseHost, 34 | Body: nil, 35 | Method: "GET", 36 | Path: fmt.Sprintf("/%s/%s/items/%s", b.service.projectId, b.Name, key), 37 | AuthToken: b.service.key, 38 | }) 39 | return NewResponse(resp, err, 200) 40 | } 41 | 42 | func (b *Base) Delete(key string) *Response { 43 | resp, err := Request(Config{ 44 | Prefix: BaseHost, 45 | Body: nil, 46 | Method: "DELETE", 47 | Path: fmt.Sprintf("/%s/%s/items/%s", b.service.projectId, b.Name, key), 48 | AuthToken: b.service.key, 49 | }) 50 | return NewResponse(resp, err, 200) 51 | } 52 | 53 | func (b *Base) Insert(record any) *Response { 54 | resp, err := Request(Config{ 55 | Prefix: BaseHost, 56 | Body: map[string]interface{}{"item": record}, 57 | Method: "POST", 58 | Path: fmt.Sprintf("/%s/%s/items", b.service.projectId, b.Name), 59 | AuthToken: b.service.key, 60 | }) 61 | return NewResponse(resp, err, 201) 62 | } 63 | 64 | func (b *Base) Update(updater *Updater) *Response { 65 | resp, err := Request(Config{ 66 | Prefix: BaseHost, 67 | Body: updater.updates, 68 | Method: "PATCH", 69 | Path: fmt.Sprintf("/%s/%s/items/%s", b.service.projectId, b.Name, updater.Key), 70 | AuthToken: b.service.key, 71 | }) 72 | return NewResponse(resp, err, 200) 73 | } 74 | 75 | func (b *Base) Fetch(query *Query) *Response { 76 | body := map[string]interface{}{"query": query.Value} 77 | if query.Limit <= 0 || query.Limit > 1000 { 78 | body["limit"] = 1000 79 | } else { 80 | body["limit"] = query.Limit 81 | } 82 | if query.Last != "" { 83 | body["last"] = query.Last 84 | } 85 | resp, err := Request(Config{ 86 | Prefix: BaseHost, 87 | Body: body, 88 | Method: "POST", 89 | Path: fmt.Sprintf("/%s/%s/query", b.service.projectId, b.Name), 90 | AuthToken: b.service.key, 91 | }) 92 | return NewResponse(resp, err, 200) 93 | } 94 | 95 | func (b *Base) FetchUntilEnd(query *Query) *Response { 96 | var container []map[string]interface{} 97 | resp := b.Fetch(query) 98 | if resp.Error != nil { 99 | return resp 100 | } 101 | var fetchData struct { 102 | Paging struct { 103 | Size float64 `json:"size"` 104 | Last string `json:"last"` 105 | } `json:"paging"` 106 | Items []map[string]interface{} `json:"items"` 107 | } 108 | _ = json.Unmarshal(resp.Body, &fetchData) 109 | container = append(container, fetchData.Items...) 110 | for { 111 | if fetchData.Paging.Last == "" { 112 | break 113 | } 114 | query.Last = fetchData.Paging.Last 115 | resp := b.Fetch(query) 116 | if resp.Error != nil { 117 | return resp 118 | } 119 | _ = json.Unmarshal(resp.Body, &fetchData) 120 | container = append(container, fetchData.Items...) 121 | if fetchData.Paging.Last == query.Last { 122 | break 123 | } 124 | } 125 | nb, _ := json.Marshal(container) 126 | return &Response{ 127 | Body: nb, 128 | StatusCode: 200, 129 | Error: nil, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /deta/deta.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type deta struct { 8 | service *service 9 | } 10 | 11 | func (d *deta) Base(name string) *Base { 12 | return &Base{Name: name, service: d.service} 13 | } 14 | 15 | func (d *deta) Drive(name string) *drive { 16 | return &drive{Name: name, service: d.service} 17 | } 18 | 19 | func New(key string) *deta { 20 | fragments := strings.Split(key, "_") 21 | if len(fragments) != 2 { 22 | panic("invalid project key is given") 23 | } 24 | service := service{key: key, projectId: fragments[0]} 25 | return &deta{service: &service} 26 | } 27 | -------------------------------------------------------------------------------- /deta/drive.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | const DriveHost = "https://drive.deta.sh/v1" 9 | const maxChunkSize = 1024 * 1024 * 10 10 | 11 | type drive struct { 12 | Name string 13 | service *service 14 | } 15 | 16 | // Put uploads the file with the given name. 17 | // If the file already exists, it is overwritten. 18 | func (d *drive) Put(saveAs string, content []byte) *Response { 19 | if len(content) <= maxChunkSize { 20 | resp, err := Request(Config{ 21 | Prefix: DriveHost, 22 | Body: content, 23 | Method: "POST", 24 | Path: fmt.Sprintf("/%s/%s/files?name=%s", d.service.projectId, d.Name, saveAs), 25 | AuthToken: d.service.key, 26 | ContentType: "application/octet-stream", 27 | }) 28 | return NewResponse(resp, err, 201) 29 | } 30 | chunks := len(content) / maxChunkSize 31 | if len(content)%maxChunkSize != 0 { 32 | chunks++ 33 | } 34 | var parts [][]byte 35 | for i := 0; i < chunks; i++ { 36 | start := i * maxChunkSize 37 | end := start + maxChunkSize 38 | if end > len(content) { 39 | end = len(content) 40 | } 41 | parts = append(parts, content[start:end]) 42 | } 43 | initResp, err := Request(Config{ 44 | Prefix: DriveHost, 45 | Method: "POST", 46 | Path: fmt.Sprintf("/%s/%s/uploads?name=%s", d.service.projectId, d.Name, saveAs), 47 | AuthToken: d.service.key, 48 | }) 49 | if err != nil { 50 | panic(err) 51 | } 52 | var resp struct { 53 | Name string `json:"name"` 54 | UploadId string `json:"upload_id"` 55 | ProjectId string `json:"project_id"` 56 | DriveName string `json:"drive_name"` 57 | } 58 | err = json.NewDecoder(initResp.Body).Decode(&resp) 59 | if err != nil { 60 | panic(err) 61 | } 62 | codes := make(chan int, len(parts)) 63 | for i, part := range parts { 64 | go func(i int, part []byte) { 65 | r, _ := Request(Config{ 66 | Prefix: DriveHost, 67 | Body: part, 68 | Method: "POST", 69 | Path: fmt.Sprintf("/%s/%s/uploads/%s/parts?name=%s&part=%d", d.service.projectId, d.Name, resp.UploadId, resp.Name, i+1), 70 | AuthToken: d.service.key, 71 | ContentType: "application/octet-stream", 72 | }) 73 | codes <- r.StatusCode 74 | }(i, part) 75 | } 76 | for i := 0; i < len(parts); i++ { 77 | <-codes 78 | } 79 | for i := 0; i < len(parts); i++ { 80 | code := <-codes 81 | if code != 200 { 82 | return NewResponse(nil, fmt.Errorf("error uploading part %d", i+1), 200) 83 | } 84 | } 85 | final, err := Request(Config{ 86 | Prefix: DriveHost, 87 | Method: "PATCH", 88 | Path: fmt.Sprintf("/%s/%s/uploads/%s?name=%s", d.service.projectId, d.Name, resp.UploadId, resp.Name), 89 | AuthToken: d.service.key, 90 | }) 91 | return NewResponse(final, err, 200) 92 | } 93 | 94 | // Get returns the file as ReadCloser with the given name. 95 | func (d *drive) Get(name string) *Response { 96 | resp, err := Request(Config{ 97 | Prefix: DriveHost, 98 | Method: "GET", 99 | Path: fmt.Sprintf("/%s/%s/files/download?name=%s", d.service.projectId, d.Name, name), 100 | AuthToken: d.service.key, 101 | }) 102 | return NewResponse(resp, err, 200) 103 | } 104 | 105 | // Delete deletes the files with the given names. 106 | func (d *drive) Delete(names ...string) *Response { 107 | resp, err := Request(Config{ 108 | Prefix: DriveHost, 109 | Method: "DELETE", 110 | Path: fmt.Sprintf("/%s/%s/files", d.service.projectId, d.Name), 111 | AuthToken: d.service.key, 112 | Body: map[string][]string{"names": names}, 113 | }) 114 | return NewResponse(resp, err, 200) 115 | } 116 | 117 | // Files returns all the files in the drive with the given prefix. 118 | // If prefix is empty, all files are returned. 119 | // limit <- the number of files to return, defaults to 1000. 120 | // last <- last filename of the previous request to get the next set of files. 121 | // Use limit 0 and last "" to obtain the default behaviour of the drive. 122 | func (d *drive) Files(prefix string, limit int, last string) *Response { 123 | if limit < 0 || limit > 1000 { 124 | limit = 1000 125 | } 126 | path := fmt.Sprintf("/%s/%s/files?limit=%d", d.service.projectId, d.Name, limit) 127 | if prefix != "" { 128 | path += fmt.Sprintf("&prefix=%s", prefix) 129 | } 130 | if last != "" { 131 | path += fmt.Sprintf("&last=%s", last) 132 | } 133 | resp, err := Request(Config{ 134 | Prefix: DriveHost, 135 | Method: "GET", 136 | Path: path, 137 | AuthToken: d.service.key, 138 | }) 139 | return NewResponse(resp, err, 200) 140 | } 141 | -------------------------------------------------------------------------------- /deta/https.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type service struct { 11 | key string 12 | projectId string 13 | } 14 | 15 | type Config struct { 16 | Prefix string 17 | Method string 18 | AuthToken string 19 | Path string 20 | ContentType string 21 | Body interface{} 22 | } 23 | 24 | func Request(config Config) (*http.Response, error) { 25 | url := config.Prefix + config.Path 26 | var body io.Reader 27 | if config.Body != nil { 28 | if b, ok := config.Body.([]byte); ok { 29 | body = bytes.NewReader(b) 30 | } else { 31 | b, _ := json.Marshal(config.Body) 32 | body = bytes.NewReader(b) 33 | } 34 | } 35 | req, err := http.NewRequest(config.Method, url, body) 36 | if config.ContentType == "" { 37 | config.ContentType = "application/json" 38 | } 39 | req.Header.Set("Content-Type", config.ContentType) 40 | req.Header.Set("X-API-Key", config.AuthToken) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return http.DefaultClient.Do(req) 45 | } 46 | -------------------------------------------------------------------------------- /deta/query.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Query struct { 8 | Last string // Last is the last key of the previous query 9 | Limit int // Limit is the maximum number of items to return 10 | Value []map[string]interface{} 11 | } 12 | 13 | func NewQuery() *Query { 14 | q := Query{ 15 | Last: "", 16 | Limit: 1000, 17 | Value: []map[string]interface{}{{}}, 18 | } 19 | return &q 20 | } 21 | 22 | func (q *Query) Equals(field string, value interface{}) { 23 | q.Value[0][field] = value 24 | } 25 | 26 | func (q *Query) NotEquals(field string, value interface{}) { 27 | q.Value[0][fmt.Sprintf("%s?ne", field)] = value 28 | } 29 | 30 | func (q *Query) GreaterThan(field string, value interface{}) { 31 | q.Value[0][fmt.Sprintf("%s?gt", field)] = value 32 | } 33 | 34 | func (q *Query) GreaterThanOrEqual(field string, value interface{}) { 35 | q.Value[0][fmt.Sprintf("%s?gte", field)] = value 36 | } 37 | 38 | func (q *Query) LessThan(field string, value interface{}) { 39 | q.Value[0][fmt.Sprintf("%s?lt", field)] = value 40 | } 41 | 42 | func (q *Query) LessThanOrEqual(field string, value interface{}) { 43 | q.Value[0][fmt.Sprintf("%s?lte", field)] = value 44 | } 45 | 46 | func (q *Query) Prefix(field string, prefix string) { 47 | q.Value[0][fmt.Sprintf("%s?pfx", field)] = prefix 48 | } 49 | 50 | func (q *Query) Range(field string, start interface{}, end interface{}) { 51 | q.Value[0][fmt.Sprintf("%s?r", field)] = []interface{}{start, end} 52 | } 53 | 54 | func (q *Query) Contains(field, substring string) { 55 | q.Value[0][fmt.Sprintf("%s?contains", field)] = substring 56 | } 57 | 58 | func (q *Query) NotContains(field, substring string) { 59 | q.Value[0][fmt.Sprintf("%s?not_contains", field)] = substring 60 | } 61 | 62 | func (q *Query) Union(queries ...Query) { 63 | for _, query := range queries { 64 | q.Value = append(q.Value, query.Value...) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /deta/updater.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | type Updater struct { 4 | Key string 5 | updates map[string]interface{} 6 | } 7 | 8 | func NewUpdater(key string) *Updater { 9 | if key == "" { 10 | panic("key cannot be empty") 11 | } 12 | return &Updater{ 13 | Key: key, 14 | updates: map[string]interface{}{ 15 | "delete": []string{}, 16 | "set": map[string]interface{}{}, 17 | "append": map[string]interface{}{}, 18 | "prepend": map[string]interface{}{}, 19 | "increment": map[string]interface{}{}, 20 | }, 21 | } 22 | } 23 | 24 | func (u *Updater) Set(field string, value interface{}) { 25 | u.updates["set"].(map[string]interface{})[field] = value 26 | } 27 | 28 | func (u *Updater) Delete(fields ...string) { 29 | u.updates["delete"] = append(u.updates["delete"].([]string), fields...) 30 | } 31 | 32 | func (u *Updater) Increment(field string, value interface{}) { 33 | u.updates["increment"].(map[string]interface{})[field] = value 34 | } 35 | 36 | func (u *Updater) Append(field string, value []interface{}) { 37 | u.updates["append"].(map[string]interface{})[field] = value 38 | } 39 | 40 | func (u *Updater) Prepend(field string, value []interface{}) { 41 | u.updates["prepend"].(map[string]interface{})[field] = value 42 | } 43 | -------------------------------------------------------------------------------- /deta/utils.go: -------------------------------------------------------------------------------- 1 | package deta 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | type Response struct { 12 | StatusCode int 13 | Body []byte 14 | Error error 15 | } 16 | 17 | func (r *Response) JSON() map[string]interface{} { 18 | var data map[string]interface{} 19 | json.Unmarshal(r.Body, &data) 20 | return data 21 | } 22 | 23 | func (r *Response) ArrayJSON() []map[string]interface{} { 24 | var data []map[string]interface{} 25 | _ = json.Unmarshal(r.Body, &data) 26 | return data 27 | } 28 | 29 | func ErrFromStatus(status, ok int) error { 30 | if status == ok { 31 | return nil 32 | } 33 | switch status { 34 | case 400: 35 | return errors.New("bad request") 36 | case 401: 37 | return errors.New("unauthorized") 38 | case 403: 39 | return errors.New("forbidden") 40 | case 404: 41 | return errors.New("not found") 42 | case 409: 43 | return errors.New("conflict") 44 | default: 45 | return fmt.Errorf("unknown error with status code %d", status) 46 | } 47 | } 48 | 49 | func NewResponse(resp *http.Response, err error, ok int) *Response { 50 | if err != nil { 51 | return &Response{Error: err} 52 | } 53 | err = ErrFromStatus(resp.StatusCode, ok) 54 | if err != nil { 55 | return &Response{Error: err} 56 | } 57 | ba, _ := io.ReadAll(resp.Body) 58 | return &Response{StatusCode: resp.StatusCode, Body: ba, Error: nil} 59 | } 60 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module backend 2 | 3 | go 1.19 4 | 5 | require github.com/gin-gonic/gin v1.9.1 6 | 7 | require ( 8 | github.com/bytedance/sonic v1.9.1 // indirect 9 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 10 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 11 | github.com/gin-contrib/sse v0.1.0 // indirect 12 | github.com/go-playground/locales v0.14.1 // indirect 13 | github.com/go-playground/universal-translator v0.18.1 // indirect 14 | github.com/go-playground/validator/v10 v10.14.0 // indirect 15 | github.com/goccy/go-json v0.10.2 // indirect 16 | github.com/json-iterator/go v1.1.12 // indirect 17 | github.com/klauspost/cpuid/v2 v2.2.5 // indirect 18 | github.com/leodido/go-urn v1.2.4 // indirect 19 | github.com/mattn/go-isatty v0.0.19 // indirect 20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 21 | github.com/modern-go/reflect2 v1.0.2 // indirect 22 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 23 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 24 | github.com/ugorji/go/codec v1.2.11 // indirect 25 | golang.org/x/arch v0.3.0 // indirect 26 | golang.org/x/crypto v0.17.0 // indirect 27 | golang.org/x/net v0.10.0 // indirect 28 | golang.org/x/sys v0.15.0 // indirect 29 | golang.org/x/text v0.14.0 // indirect 30 | google.golang.org/protobuf v1.30.0 // indirect 31 | gopkg.in/yaml.v3 v3.0.1 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 2 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 3 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= 11 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= 12 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 13 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 14 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 15 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 16 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 17 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 18 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 19 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 20 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 21 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= 22 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= 23 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 24 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 25 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 26 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 29 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 30 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 31 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 32 | github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= 33 | github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 34 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= 35 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= 36 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 37 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 39 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 40 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 41 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 42 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 43 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 44 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 45 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 46 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 47 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 48 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 49 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 50 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 51 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 52 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 53 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 54 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 55 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 56 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 57 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 58 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 59 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 60 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= 61 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 62 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 63 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 64 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 65 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 66 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 67 | golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= 68 | golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 69 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 71 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 72 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 73 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 74 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 75 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 76 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 77 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 78 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 79 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 81 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 83 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 84 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 85 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 86 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func main() { 11 | gin.SetMode(gin.ReleaseMode) 12 | app := gin.Default() 13 | app.GET("/", func(c *gin.Context) { 14 | c.File("./static/pages/app.html") 15 | }) 16 | app.GET("/manifest.json", func(c *gin.Context) { 17 | c.File("manifest.json") 18 | }) 19 | app.GET("/worker.js", func(c *gin.Context) { 20 | c.File("worker.js") 21 | }) 22 | app.GET("/shared/:hash", SharedPage) 23 | app.GET("/embed/:hash", EmbedFile) 24 | app.Static("/static", "static") 25 | app.POST("/__space/v0/actions", Job) 26 | app.GET("/__space/actions", AppActions) 27 | 28 | api := app.Group("/api") 29 | api.GET("/ping", Ping) 30 | api.GET("/key", ProjectKey) 31 | api.GET("/microid", MicroId) 32 | api.GET("/sanitize", SanitizeFiles) 33 | api.Any("/metadata", Metadata) 34 | api.GET("/metadata/:hash", SharedMeta) 35 | api.POST("/query", Query) 36 | api.GET("/consumption", Consumption) 37 | api.POST("/count/items", FolderChildrenCount) 38 | api.Any("/bulk", FileBulkOps) 39 | api.GET("/download/:recipient/:hash/:part", DownloadFile) 40 | api.GET("/external/:recipient/:owner/:hash/:part", DownloadFileExtern) 41 | api.POST("/push/:id", PushFileMeta) 42 | api.POST("/accept", AcceptFileMeta) 43 | api.POST("/v2/migrate", MigrateV2) 44 | 45 | actions := app.Group("/actions") 46 | actions.POST("/save", Save.Handler) 47 | 48 | if err := app.Run(":8080"); err != http.ErrServerClosed { 49 | log.Fatal(err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "com.filebox.app", 3 | "name": "Filebox", 4 | "short_name": "Filebox", 5 | "description": "Upload & organize files in your Personal Space Drive", 6 | "start_url": "/", 7 | "scope": ".", 8 | "icons": [ 9 | { 10 | "src": "/static/assets/icon.png", 11 | "sizes": "512x512", 12 | "type": "image/png" 13 | }, 14 | { 15 | "src": "/static/assets/app_icon.png", 16 | "sizes": "512x512", 17 | "type": "image/png", 18 | "purpose": "maskable" 19 | } 20 | ], 21 | "theme_color": "#0561da", 22 | "background_color": "white", 23 | "display": "standalone" 24 | } -------------------------------------------------------------------------------- /previews/desktop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/filebox/a827296338dc0cd744e02e9d7713b439250b50c9/previews/desktop.png -------------------------------------------------------------------------------- /previews/mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/filebox/a827296338dc0cd744e02e9d7713b439250b50c9/previews/mobile.png -------------------------------------------------------------------------------- /static/assets/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/filebox/a827296338dc0cd744e02e9d7713b439250b50c9/static/assets/app_icon.png -------------------------------------------------------------------------------- /static/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jnsougata/filebox/a827296338dc0cd744e02e9d7713b439250b50c9/static/assets/icon.png -------------------------------------------------------------------------------- /static/pages/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 |You don't have any file or folder
`; 101 | } 102 | }); 103 | 104 | const browseButton = document.querySelector("#browse"); 105 | browseButton.addEventListener("click", async () => { 106 | openedOptionGL = "browse"; 107 | openedFolderGL = null; 108 | blurLayer.click(); 109 | const resp = await fetch(`/api/query`, { 110 | method: "POST", 111 | body: JSON.stringify({ parent: null, "deleted?ne": true }), 112 | }); 113 | const data = await resp.json(); 114 | view.innerHTML = ""; 115 | if (!data && !localStorage.getItem("isGreeted")) { 116 | renderGreetings(); 117 | return; 118 | } else if (!data) { 119 | view.innerHTML = `You don't have any file or folder
`; 120 | return; 121 | } 122 | let files = []; 123 | let folders = []; 124 | data.forEach((file) => { 125 | file.type === "folder" ? folders.push(file) : files.push(file); 126 | }); 127 | view.appendChild(buildPrompt({ parent: null })); 128 | let list = document.createElement("ul"); 129 | view.appendChild(list); 130 | folders.concat(files).forEach((file) => { 131 | list.appendChild(newFileElem(file)); 132 | }); 133 | updateFolderStats(folders); 134 | document.querySelector(".fragment").innerText = "home"; 135 | }); 136 | 137 | const pinnedButton = document.querySelector("#pinned"); 138 | pinnedButton.addEventListener("click", async () => { 139 | openedFolderGL = null; 140 | openedOptionGL = "pinned"; 141 | blurLayer.click(); 142 | const resp = await fetch("/api/query", { 143 | method: "POST", 144 | body: JSON.stringify({ pinned: true, "deleted?ne": true }), 145 | }); 146 | let data = await resp.json(); 147 | view.innerHTML = ""; 148 | if (!data) { 149 | view.innerHTML = `You don't have any pinned file or folder
`; 150 | return; 151 | } 152 | let files = []; 153 | let folders = []; 154 | data.forEach((file) => { 155 | file.type === "folder" ? folders.push(file) : files.push(file); 156 | }); 157 | let list = document.createElement("ul"); 158 | view.appendChild(list); 159 | folders.concat(files).forEach((file) => { 160 | list.appendChild(newFileElem(file)); 161 | }); 162 | }); 163 | 164 | const sharedButton = document.querySelector("#shared"); 165 | sharedButton.addEventListener("click", async () => { 166 | openedFolderGL = null; 167 | openedOptionGL = "shared"; 168 | blurLayer.click(); 169 | view.innerHTML = ""; 170 | let fileList = document.createElement("div"); 171 | fileList.className = "file_list"; 172 | const resp = await fetch(`/api/query`, { 173 | method: "POST", 174 | body: JSON.stringify({ parent: "~shared" }), 175 | }); 176 | let data = await resp.json(); 177 | if (!data) { 178 | view.innerHTML = `You haven't received any file
`; 179 | return; 180 | } 181 | let list = document.createElement("ul"); 182 | view.appendChild(list); 183 | data.forEach((file) => { 184 | list.appendChild(newFileElem(file)); 185 | }); 186 | }); 187 | 188 | const trashButton = document.querySelector("#trash"); 189 | trashButton.addEventListener("click", async () => { 190 | openedFolderGL = null; 191 | openedOptionGL = "trash"; 192 | blurLayer.click(); 193 | const resp = await fetch("/api/query", { 194 | method: "POST", 195 | body: JSON.stringify({ deleted: true }), 196 | }); 197 | const data = await resp.json(); 198 | view.innerHTML = ""; 199 | if (!data) { 200 | view.innerHTML = `There is no trash file
`; 201 | return; 202 | } 203 | dataMap = {}; 204 | data.forEach((file) => { 205 | dataMap[file.hash] = file; 206 | }); 207 | trashFilesGL = dataMap; 208 | let list = document.createElement("ul"); 209 | view.appendChild(list); 210 | data.forEach((file) => { 211 | list.appendChild(newFileElem(file, true)); 212 | }); 213 | }); 214 | 215 | const migrateV2Button = document.querySelector("#migrateV2"); 216 | migrateV2Button?.addEventListener("click", async () => { 217 | openedFolderGL = null; 218 | openedOptionGL = "trash"; 219 | blurLayer.click(); 220 | const resp = await fetch("/api/query", { 221 | method: "POST", 222 | body: JSON.stringify({}) 223 | }); 224 | migrationModal.innerHTML = ""; 225 | migrationModal.style.display = "flex"; 226 | migrationModal.style.flexDirection = "column"; 227 | migrationModal.style.justifyContent = "flex-start"; 228 | migrationModal.style.outline = "none"; 229 | const data = await resp.json(); 230 | let h3 = document.createElement("h3"); 231 | h3.innerHTML = `Migrating ${data.length} files`; 232 | migrationModal.appendChild(h3); 233 | let counter = 0; 234 | data.forEach(async (file) => { 235 | migrationModal.showModal(); 236 | const r = await fetch(`/api/v2/migrate`, { 237 | method: "POST", 238 | body: JSON.stringify(file) 239 | }); 240 | let p = document.createElement("p"); 241 | if (r.status === 207) { 242 | p.innerHTML = `${++counter}. ${file.name} migrated successfully ✔`; 243 | } else { 244 | p.innerHTML = `${++counter}. ${file.name} failed to migrate 𐄂`; 245 | } 246 | migrationModal.appendChild(p); 247 | migrationModal.scrollTop = migrationModal.scrollHeight; 248 | }) 249 | let close = document.createElement("button"); 250 | close.innerHTML = `close`; 251 | close.addEventListener("click", () => { 252 | migrationModal.style.display = "none"; 253 | migrationModal.close(); 254 | }) 255 | migrationModal.appendChild(close); 256 | }) 257 | 258 | const themeButton = document.querySelector("#theme"); 259 | themeButton.addEventListener("click", async () => { 260 | let lightMode = localStorage.getItem("light-mode"); 261 | if (lightMode === "true") { 262 | localStorage.setItem("light-mode", false); 263 | document.body.classList.remove("light-mode"); 264 | themeButton.innerHTML = "light_mode"; 265 | } else { 266 | localStorage.setItem("light-mode", true); 267 | document.body.classList.add("light-mode"); 268 | themeButton.innerHTML = "dark_mode"; 269 | } 270 | }); 271 | 272 | const queueButton = document.querySelector("#queue"); 273 | queueButton.addEventListener("click", () => { 274 | closeSidebar(); 275 | renderQueue(); 276 | }); 277 | 278 | usernameElem.addEventListener("click", () => { 279 | navigator.clipboard.writeText(userIdGL).then(() => { 280 | showSnack("User Id copied to clipboard", COLOR_GREEN, "success"); 281 | }); 282 | }); 283 | 284 | let logo = document.querySelector(".logo") 285 | logo.addEventListener("click", async () => { 286 | window.open("https://github.com/jnsougata/filebox", "_blank"); 287 | }) 288 | 289 | menuElem.addEventListener("click", (e) => { 290 | navLeft.style.display = "flex"; 291 | blurLayer.style.display = "block"; 292 | }); 293 | 294 | view.addEventListener("dragover", (e) => { 295 | e.preventDefault(); 296 | e.stopPropagation(); 297 | }); 298 | 299 | view.addEventListener("drop", (e) => { 300 | e.preventDefault(); 301 | if (e.dataTransfer.items) { 302 | [...e.dataTransfer.items].forEach((item) => { 303 | let file = item.getAsFile(); 304 | let metadata = buildFileMetadata(file); 305 | prependQueueElem(metadata, true); 306 | upload(file, metadata, (percentage) => { 307 | progressHandlerById(metadata.hash, percentage); 308 | }); 309 | }); 310 | } 311 | }); 312 | 313 | blurLayer.addEventListener("click", () => { 314 | closeSidebar(); 315 | hideRightNav(); 316 | }); 317 | 318 | window.addEventListener("DOMContentLoaded", async () => { 319 | await fetch("/api/sanitize"); 320 | browseButton.click(); 321 | let resp = await fetch(`/api/key`); 322 | let data = await resp.json(); 323 | secretKeyGL = data.key; 324 | resp = await fetch("/api/consumption"); 325 | data = await resp.json(); 326 | updateSpaceUsage(data.size); 327 | resp = await fetch("/api/microid"); 328 | data = await resp.json(); 329 | userIdGL = data.id; 330 | usernameElem.innerHTML = userIdGL; 331 | }); 332 | 333 | window.addEventListener("load", () => { 334 | if ("serviceWorker" in navigator) { 335 | navigator.serviceWorker.register("/worker.js"); 336 | } else { 337 | console.log("Service worker not supported"); 338 | } 339 | if (localStorage.getItem("light-mode") === "true") { 340 | document.body.classList.add("light-mode"); 341 | themeButton.innerHTML = "dark_mode"; 342 | } 343 | }); 344 | 345 | document.addEventListener("click", (e) => { 346 | if (e.target.tagName === "DIALOG" && fileContextMenuGL) { 347 | fileContextMenuGL.close(); 348 | } 349 | }); 350 | 351 | window.addEventListener("resize", () => { 352 | if (window.innerWidth > 768) { 353 | menuElem.style.display = "none"; 354 | searchIcon.style.display = "flex"; 355 | navLeft.style.display = "flex"; 356 | } else { 357 | menuElem.style.display = "flex"; 358 | searchIcon.style.display = "none"; 359 | navLeft.style.display = "none"; 360 | if (fileContextMenuGL) { 361 | fileContextMenuGL.close(); 362 | } 363 | } 364 | blurLayer.click(); 365 | }); 366 | 367 | window.addEventListener("paste", (e) => { 368 | let items = e.clipboardData.items; 369 | if (items.length) { 370 | [...items].forEach((item) => { 371 | if (item.kind === "file") { 372 | let file = item.getAsFile(); 373 | let metadata = buildFileMetadata(file); 374 | prependQueueElem(metadata, true); 375 | upload(file, metadata, (percentage) => { 376 | progressHandlerById(metadata.hash, percentage); 377 | }); 378 | } 379 | }); 380 | } 381 | }); 382 | 383 | window.addEventListener("beforeunload", (e) => { 384 | if (taskCountGL > 0) { 385 | e.preventDefault(); 386 | e.returnValue = ""; 387 | } 388 | }); 389 | 390 | let searched = false; 391 | let searchInputTimer = null; 392 | searchQueryInput.addEventListener("input", (ev) => { 393 | if (searchInputTimer) { 394 | clearTimeout(searchInputTimer); 395 | } 396 | searchInputTimer = setTimeout(() => { 397 | if (ev.target.value.length > 0) { 398 | searched = true; 399 | clearQueryButton.style.display = "flex"; 400 | let matches = /:(.*?) (.*)/.exec(ev.target.value); 401 | if (matches) { 402 | let attr = matches[1]; 403 | let contains = matches[2]; 404 | renderSearchResults({ [`${attr}?contains`]: `${contains}` }); 405 | } else { 406 | renderSearchResults({ "name?contains": `${ev.target.value}` }); 407 | } 408 | } 409 | }, 500); 410 | }); 411 | 412 | newFolderButton.addEventListener("click", () => { 413 | createFolder(); 414 | }); 415 | 416 | clearQueryButton.addEventListener("click", () => { 417 | clearQueryButton.style.display = "none"; 418 | currentOption().click(); 419 | searchQueryInput.value = ""; 420 | }); 421 | 422 | fileUploadButton.addEventListener("click", () => { 423 | hiddenFileInput.click(); 424 | }); 425 | 426 | folderUploadButton.addEventListener("click", () => { 427 | hiddenFolderInput.click(); 428 | }); 429 | 430 | hiddenFileInput.addEventListener("change", (ev) => { 431 | queueButton.click(); 432 | for (let i = 0; i < ev.target.files.length; i++) { 433 | let file = ev.target.files[i]; 434 | let metadata = buildFileMetadata(file); 435 | prependQueueElem(metadata, true); 436 | upload(file, metadata, (percentage) => { 437 | progressHandlerById(metadata.hash, percentage); 438 | }); 439 | } 440 | }); 441 | 442 | hiddenFolderInput.addEventListener("change", (ev) => { 443 | let relativePaths = []; 444 | for (let i = 0; i < ev.target.files.length; i++) { 445 | relativePaths.push(ev.target.files[i].webkitRelativePath); 446 | } 447 | let uniqueFolders = []; 448 | for (let i = 0; i < relativePaths.length; i++) { 449 | let folderPath = relativePaths[i].split("/"); 450 | folderPath.pop(); 451 | folderPath = folderPath.join("/"); 452 | if (!uniqueFolders.includes(folderPath)) { 453 | uniqueFolders.push(folderPath); 454 | } 455 | } 456 | let parents = []; 457 | uniqueFolders.forEach((folder) => { 458 | let folderPath = folder.split("/"); 459 | let currentPath = ""; 460 | folderPath.forEach((folder) => { 461 | currentPath += folder + "/"; 462 | if (!parents.includes(currentPath)) { 463 | parents.push(currentPath); 464 | } 465 | }); 466 | }); 467 | let strippedParents = parents.map((parent) => { 468 | return parent.slice(0, -1); 469 | }); 470 | strippedParents.forEach((parent) => { 471 | let relativePath; 472 | if (openedFolderGL) { 473 | if (openedFolderGL.parent) { 474 | relativePath = `${openedFolderGL.parent}/${openedFolderGL.name}`; 475 | } else { 476 | relativePath = openedFolderGL.name; 477 | } 478 | } 479 | let folderName; 480 | let folderPath = ""; 481 | if (parent.includes("/")) { 482 | let parentParts = parent.split("/"); 483 | folderName = parentParts.pop(); 484 | folderPath = `${parentParts.join("/")}`; 485 | } else { 486 | folderName = parent; 487 | } 488 | if (relativePath && folderPath) { 489 | folderPath = `${relativePath}/${folderPath}`; 490 | } else if (relativePath) { 491 | folderPath = relativePath; 492 | } 493 | let body = { 494 | name: folderName, 495 | type: "folder", 496 | hash: randId(), 497 | date: new Date().toISOString(), 498 | }; 499 | body.parent = folderPath ? folderPath : null; 500 | fetch(`/api/metadata`, { method: "POST", body: JSON.stringify(body) }); 501 | }); 502 | for (let i = 0; i < ev.target.files.length; i++) { 503 | let file = ev.target.files[i]; 504 | let relativePath = ev.target.files[i].webkitRelativePath; 505 | let parentFragments = relativePath.split("/"); 506 | parentFragments.pop(); 507 | let parent = parentFragments.join("/"); 508 | if (openedFolderGL) { 509 | if (openedFolderGL.parent) { 510 | parent = `${openedFolderGL.parent}/${openedFolderGL.name}/${parent}`; 511 | } else { 512 | parent = `${openedFolderGL.name}/${parent}`; 513 | } 514 | } 515 | let metadata = buildFileMetadata(file); 516 | metadata.parent = parent; 517 | prependQueueElem(metadata, true); 518 | upload( 519 | file, 520 | metadata, 521 | (percentage) => { 522 | progressHandlerById(metadata.hash, percentage); 523 | }, 524 | false 525 | ); 526 | } 527 | }); 528 | 529 | 530 | /////////////////////// File Context Menu /////////////////////// 531 | 532 | function onSendClick(file) { 533 | renderFileSenderModal(file); 534 | } 535 | 536 | function onRenameClick(file) { 537 | showRenameModal(file); 538 | } 539 | 540 | function onDownloadClick(file) { 541 | prependQueueElem(file, false); 542 | download(file, (progress) => { 543 | progressHandlerById(file.hash, progress); 544 | }); 545 | } 546 | 547 | function onShareLinkClick(file) { 548 | if (file.access === "private") { 549 | showSnack(`Make file public to share via link`, COLOR_ORANGE, "warning"); 550 | } else { 551 | window.navigator.clipboard 552 | .writeText(`${window.location.origin}/shared/${file.hash}`) 553 | .then(() => { 554 | showSnack(`Copied sharing URL to clipboard`, COLOR_GREEN, "success"); 555 | }); 556 | } 557 | } 558 | 559 | function onEmbedClick(file) { 560 | if (file.access === "private") { 561 | showSnack(`Make file public to embed`, COLOR_ORANGE, "warning"); 562 | } else if (file.size > 1024 * 1024 * 4) { 563 | showSnack(`File is too large (> 4MB) to embed`, COLOR_RED, "error"); 564 | } else { 565 | window.navigator.clipboard 566 | .writeText(`${window.location.origin}/embed/${file.hash}`) 567 | .then(() => { 568 | showSnack(`Copied embed URL to clipboard`, COLOR_GREEN, "success"); 569 | }); 570 | } 571 | } 572 | 573 | function onMoveClick(file) { 574 | isFileMovingGL = true; 575 | browseButton.click(); 576 | renderAuxNav(fileMover(file)); 577 | } 578 | 579 | function onTrashClick(file) { 580 | if (file.type === "folder") { 581 | deleteFolderPermanently(file).then(() => { 582 | showSnack(`Permanently Deleted ${file.name}`, COLOR_RED, "warning"); 583 | }); 584 | } else { 585 | file.deleted = true; 586 | fetch(`/api/metadata`, { 587 | method: "PUT", 588 | body: JSON.stringify(file), 589 | }).then(() => { 590 | showSnack(`Moved to trash ${file.name}`, COLOR_RED, "warning"); 591 | document.getElementById(`file-${file.hash}`).remove(); 592 | }); 593 | } 594 | } 595 | 596 | function onColorClick(file) { 597 | let pickerElem = document.createElement("input"); 598 | pickerElem.type = "color"; 599 | pickerElem.style.display = "none"; 600 | pickerElem.value = file.color || "#ccc"; 601 | pickerElem.addEventListener("change", () => { 602 | file.color = pickerElem.value; 603 | file.project_id = globalProjectId; 604 | fetch(`/api/metadata`, { 605 | method: "PUT", 606 | body: JSON.stringify(file), 607 | }).then(() => { 608 | let folder = document.getElementById(`file-${file.hash}`); 609 | let folderIcon = folder.children[0]; 610 | folderIcon.style.color = file.color; 611 | showSnack(`Folder color changed successfully`, COLOR_GREEN, "success"); 612 | }); 613 | }); 614 | document.body.appendChild(pickerElem); 615 | pickerElem.click(); 616 | } 617 | 618 | function onRestoreClick(file) { 619 | checkFileParentExists(file).then((exists) => { 620 | if (!exists && file.parent !== undefined) { 621 | showSnack(`Parent not found. Restoring to root`, COLOR_ORANGE, "warning"); 622 | delete file.parent; 623 | delete file.deleted; 624 | } else { 625 | delete file.deleted; 626 | } 627 | fetch(`/api/metadata`, { 628 | method: "PUT", 629 | body: JSON.stringify(file), 630 | }).then(() => { 631 | document.getElementById(`file-${file.hash}`).remove(); 632 | showSnack(`Restored ${file.name}`, COLOR_GREEN, "success"); 633 | delete trashFilesGL[file.hash]; 634 | }); 635 | }); 636 | } 637 | 638 | function onDeletePermanentlyClick(file) { 639 | fetch(`/api/metadata`, { method: "DELETE", body: JSON.stringify(file) }).then( 640 | () => { 641 | showSnack(`Permanently Deleted ${file.name}`, COLOR_RED, "warning"); 642 | document.getElementById(`file-${file.hash}`).remove(); 643 | if (!file.shared) { 644 | updateSpaceUsage(-file.size); 645 | } 646 | delete trashFilesGL[file.hash]; 647 | } 648 | ); 649 | } 650 | 651 | function onPinUnpinClick(file) { 652 | if (file.pinned) { 653 | fetch(`/api/metadata`, { 654 | method: "PATCH", 655 | body: JSON.stringify({ hash: file.hash, pinned: false }), 656 | }).then(() => { 657 | showSnack(`File unpinned successfully`, COLOR_ORANGE, "info"); 658 | let card = document.getElementById(`file-${file.hash}`); 659 | if (card) { 660 | card.remove(); 661 | } 662 | delete file.pinned; 663 | }); 664 | } else { 665 | fetch(`/api/metadata`, { 666 | method: "PATCH", 667 | body: JSON.stringify({ hash: file.hash, pinned: true }), 668 | }).then(() => { 669 | showSnack(`File pinned successfully`, COLOR_GREEN, "success"); 670 | let pinnedSection = document.querySelector(".pinned_files"); 671 | if (pinnedSection) { 672 | pinnedSection.appendChild(newFileElem(file)); 673 | } 674 | file.pinned = true; 675 | }); 676 | } 677 | } 678 | 679 | const contextOptions = [ 680 | { 681 | label: "Send", 682 | icon: "send", 683 | callback: onSendClick, 684 | fileOnly: true, 685 | }, 686 | { 687 | label: "Rename", 688 | icon: "edit", 689 | callback: onRenameClick, 690 | fileOnly: true, 691 | }, 692 | { 693 | label: "Download", 694 | icon: "download", 695 | callback: onDownloadClick, 696 | fileOnly: true, 697 | }, 698 | { 699 | label: "Share Link", 700 | icon: "link", 701 | callback: onShareLinkClick, 702 | ownerOnly: true, 703 | }, 704 | { 705 | label: "Embed Link", 706 | icon: "code", 707 | callback: onEmbedClick, 708 | fileOnly: true, 709 | ownerOnly: true, 710 | }, 711 | { 712 | label: "Move", 713 | icon: "arrow_forward", 714 | callback: onMoveClick, 715 | fileOnly: true, 716 | }, 717 | { 718 | label: "Color", 719 | icon: "color_lens", 720 | callback: onColorClick, 721 | folderOnly: true, 722 | }, 723 | { 724 | label: "Download as Zip", 725 | icon: "archive", 726 | callback: downloadFolderAsZip, 727 | folderOnly: true, 728 | }, 729 | { 730 | label: "Trash", 731 | icon: "delete", 732 | callback: onTrashClick, 733 | fileOnly: true, 734 | }, 735 | { 736 | label: "Delete Permanently", 737 | icon: "delete", 738 | callback: onTrashClick, 739 | folderOnly: true, 740 | }, 741 | { 742 | label: "Restore", 743 | icon: "replay", 744 | callback: onRestoreClick, 745 | trashOnly: true, 746 | }, 747 | { 748 | label: "Delete Permanently", 749 | icon: "delete_forever", 750 | callback: onDeletePermanentlyClick, 751 | trashOnly: true, 752 | }, 753 | ]; 754 | 755 | class FileContextMenu { 756 | constructor(event, file) { 757 | this.file = file; 758 | this.event = event; 759 | this.options = contextOptions; 760 | this.elem = document.querySelector(".context_menu"); 761 | } 762 | 763 | buildItem(label, icon) { 764 | let li = document.createElement("li"); 765 | let p = document.createElement("p"); 766 | p.innerHTML = label; 767 | let span = document.createElement("span"); 768 | span.classList.add("material-symbols-rounded"); 769 | span.innerHTML = icon; 770 | li.appendChild(p); 771 | li.appendChild(span); 772 | return li; 773 | } 774 | 775 | build() { 776 | let ul = document.createElement("ul"); 777 | let li = this.file.pinned 778 | ? this.buildItem("Unpin", "remove") 779 | : this.buildItem("Pin", "add"); 780 | li.addEventListener("click", () => { 781 | this.close(); 782 | onPinUnpinClick(this.file); 783 | }); 784 | if (!this.file.deleted) { 785 | ul.appendChild(li); 786 | } 787 | for (let option of this.options) { 788 | if (this.file.deleted && !option.trashOnly) { 789 | continue; 790 | } 791 | if (!this.file.deleted && option.trashOnly) { 792 | continue; 793 | } 794 | if (this.file.shared && option.ownerOnly) { 795 | continue; 796 | } 797 | if (this.file.type === "folder" && option.fileOnly) { 798 | continue; 799 | } 800 | if (this.file.type !== "folder" && option.folderOnly) { 801 | continue; 802 | } 803 | let li = this.buildItem(option.label, option.icon); 804 | li.addEventListener("click", () => { 805 | this.close(); 806 | option.callback(this.file); 807 | }); 808 | ul.appendChild(li); 809 | } 810 | return ul; 811 | } 812 | 813 | show() { 814 | this.elem.showModal(); 815 | this.elem.style.display = "flex"; 816 | this.elem.style.left = `${this.event.pageX}px`; 817 | this.elem.style.top = `${this.event.pageY}px`; 818 | let menuRect = this.elem.getBoundingClientRect(); 819 | let windowWidth = window.innerWidth; 820 | let windowHeight = window.innerHeight; 821 | if (menuRect.right > windowWidth) { 822 | this.elem.style.left = `${windowWidth - menuRect.width}px`; 823 | } 824 | if (menuRect.bottom > windowHeight) { 825 | this.elem.style.top = `${windowHeight - menuRect.height}px`; 826 | } 827 | this.elem.innerHTML = ""; 828 | this.elem.appendChild(this.build()); 829 | let parent = this.event.target.parentElement; 830 | while (parent.tagName !== "LI") { 831 | parent = parent.parentElement; 832 | } 833 | parent.style.backgroundColor = `var(--color-blackish-hover)`; 834 | this.elem.id = this.file.hash; 835 | fileContextMenuGL = this; 836 | } 837 | 838 | close() { 839 | this.elem.close(); 840 | this.elem.style.display = "none"; 841 | const fileElem = document.getElementById(`file-${this.elem.id}`); 842 | if (fileElem) { 843 | fileElem.style.backgroundColor = `transparent`; 844 | } 845 | fileContextMenuGL = null; 846 | } 847 | } 848 | 849 | /////////////////////// Queue Util /////////////////////// 850 | 851 | function prependQueueElem(file, isUpload = true) { 852 | let li = document.createElement("li"); 853 | let icon = document.createElement("div"); 854 | icon.className = "icon"; 855 | setIconByMime(file.mime, icon); 856 | if (isUpload === null) { 857 | icon.innerHTML = 858 | 'open_in_browser'; 859 | } 860 | let info = document.createElement("div"); 861 | info.className = "info"; 862 | let name = document.createElement("p"); 863 | name.innerHTML = file.name; 864 | let progress = document.createElement("div"); 865 | progress.className = "progress"; 866 | let bar = document.createElement("div"); 867 | bar.className = "bar"; 868 | bar.style.width = "0%"; 869 | if (isUpload === null) { 870 | bar.style.backgroundColor = COLOR_FILE_LOAD; 871 | } else if (isUpload) { 872 | bar.style.backgroundColor = COLOR_BLUE; 873 | } else { 874 | bar.style.backgroundColor = COLOR_GREEN; 875 | } 876 | bar.id = `bar-${file.hash}`; 877 | progress.appendChild(bar); 878 | info.appendChild(name); 879 | info.appendChild(progress); 880 | let percentage = document.createElement("p"); 881 | percentage.innerHTML = "0%"; 882 | percentage.id = `percentage-${file.hash}`; 883 | li.appendChild(icon); 884 | li.appendChild(info); 885 | li.appendChild(percentage); 886 | taskFactoryGL[file.hash] = { 887 | element: li, 888 | index: taskCountGL + 1, 889 | bar: bar, 890 | percentage: percentage, 891 | }; 892 | taskCountGL++; 893 | renderQueue(); 894 | } 895 | 896 | function renderQueue() { 897 | let queue = document.createElement("div"); 898 | queue.className = "queue"; 899 | let close = document.createElement("div"); 900 | close.className = "queue_close"; 901 | close.innerHTML = 902 | 'chevron_right'; 903 | close.addEventListener("click", () => { 904 | hideRightNav(); 905 | }); 906 | let content = document.createElement("div"); 907 | content.className = "queue_content"; 908 | let p = document.createElement("p"); 909 | p.innerHTML = "Activities"; 910 | let tasks = document.createElement("ul"); 911 | let sortedNodes = Object.values(taskFactoryGL).sort((a, b) => b.index - a.index); 912 | for (let node of sortedNodes) { 913 | tasks.appendChild(node.element); 914 | } 915 | content.appendChild(p); 916 | content.appendChild(tasks); 917 | queue.appendChild(close); 918 | queue.appendChild(content); 919 | renderInRightNav(queue); 920 | } 921 | 922 | /////////////////////// File Transport Utils /////////////////////// 923 | 924 | async function partUploader(hash, index, content, totalSize, handler) { 925 | console.log(`Uploading part ${index} of ${hash}`); 926 | const resp = await fetch(`/api/upload/${hash}/${index}`, { 927 | method: "PUT", 928 | body: content, 929 | }) 930 | handler() 931 | if (resp.status === 200) { 932 | updateSpaceUsage(content.byteLength) 933 | handler((content.byteLength / totalSize) * 100) 934 | } 935 | } 936 | 937 | async function chunkedUpload(file, metadata, progressHandler) { 938 | let hash = metadata.hash; 939 | const reader = new FileReader(); 940 | reader.onload = async (ev) => { 941 | const content = ev.target.result; 942 | let chunks = []; 943 | let chunkSize = 2 * 1024 * 1024; 944 | for (let i = 0; i < content.byteLength; i += chunkSize) { 945 | chunks.push({ 946 | index: i / chunkSize, 947 | content: content.slice(i, i + chunkSize) 948 | }); 949 | } 950 | let allOk = true; 951 | let batchSize = 5; 952 | let batches = []; 953 | for (let i = 0; i < chunks.length; i += batchSize) { 954 | batches.push(chunks.slice(i, i + batchSize)); 955 | } 956 | for (let i = 0; i < batches.length; i++) { 957 | let promises = []; 958 | let batch = batches[i]; 959 | batch.forEach((chunk, _) => { 960 | promises.push( 961 | partUploader(hash, chunk.index, chunk.content, file.size, progressHandler) 962 | ); 963 | }); 964 | await Promise.all(promises); 965 | } 966 | if (allOk) { 967 | await fetch(`/api/metadata`, { 968 | method: "POST", 969 | body: JSON.stringify(metadata), 970 | }); 971 | progressHandler(100); 972 | showSnack(`Uploaded ${file.name}`, COLOR_BLUE, "success"); 973 | openedFolderGL 974 | ? handleFolderClick(openedFolderGL) 975 | : currentOption().click(); 976 | hideRightNav(); 977 | } 978 | }; 979 | reader.readAsArrayBuffer(file); 980 | } 981 | 982 | function upload(file, metadata, progressHandler, refreshList = true) { 983 | let hash = metadata.hash; 984 | let header = { "X-Api-Key": secretKeyGL, "Content-Type": file.type }; 985 | let projectId = secretKeyGL.split("_")[0]; 986 | const ROOT = "https://drive.deta.sh/v1"; 987 | let reader = new FileReader(); 988 | reader.onload = async (ev) => { 989 | progressHandler(0); 990 | showSnack(`Uploading ${file.name}`, COLOR_BLUE, "info"); 991 | let content = ev.target.result; 992 | let nameFragments = file.name.split("."); 993 | let saveAs = 994 | nameFragments.length > 1 ? `${hash}.${nameFragments.pop()}` : `${hash}`; 995 | const chunkSize = 10 * 1024 * 1024; 996 | if (file.size < chunkSize) { 997 | await fetch(`${ROOT}/${projectId}/filebox/files?name=${saveAs}`, { 998 | method: "POST", 999 | body: content, 1000 | headers: header, 1001 | }); 1002 | const resp = await fetch(`/api/metadata`, { 1003 | method: "POST", 1004 | body: JSON.stringify(metadata), 1005 | }); 1006 | progressHandler(100); 1007 | showSnack(`Uploaded ${file.name}`, COLOR_BLUE, "success"); 1008 | updateSpaceUsage(file.size); 1009 | if (!refreshList) { 1010 | return; 1011 | } 1012 | openedFolderGL 1013 | ? handleFolderClick(openedFolderGL) 1014 | : currentOption().click(); 1015 | hideRightNav(); 1016 | } else { 1017 | const resp = await fetch( 1018 | `${ROOT}/${projectId}/filebox/uploads?name=${saveAs}`, 1019 | { 1020 | method: "POST", 1021 | headers: header, 1022 | } 1023 | ); 1024 | const data = await resp.json(); 1025 | let chunks = []; 1026 | for (let i = 0; i < content.byteLength; i += chunkSize) { 1027 | chunks.push({ 1028 | index: i / chunkSize, 1029 | content: content.slice(i, i + chunkSize) 1030 | }); 1031 | } 1032 | let allOk = true; 1033 | let batches = []; 1034 | let name = data.name; 1035 | let uploadId = data["upload_id"]; 1036 | const batchSize = 5; 1037 | for (let i = 0; i < chunks.length; i += batchSize) { 1038 | batches.push(chunks.slice(i, i + batchSize)); 1039 | } 1040 | let progress = 0; 1041 | async function uploadPart(chunk) { 1042 | let resp = await fetch( 1043 | `${ROOT}/${projectId}/filebox/uploads/${uploadId}/parts?name=${name}&part=${ 1044 | chunk.index + 1 1045 | }`, 1046 | { 1047 | method: "POST", 1048 | body: chunk.content, 1049 | headers: header, 1050 | } 1051 | ) 1052 | if (resp.status !== 200) { 1053 | allOk = false; 1054 | return 1055 | } 1056 | progress += chunk.content.byteLength 1057 | progressHandler(Math.round((progress / file.size) * 100)); 1058 | } 1059 | for (let i = 0; i < batches.length; i++) { 1060 | let promises = []; 1061 | let batch = batches[i]; 1062 | batch.forEach((chunk, _) => { 1063 | promises.push(uploadPart(chunk)); 1064 | }); 1065 | await Promise.all(promises); 1066 | } 1067 | if (allOk) { 1068 | await fetch( 1069 | `${ROOT}/${projectId}/filebox/uploads/${uploadId}?name=${name}`, 1070 | { 1071 | method: "PATCH", 1072 | headers: header, 1073 | } 1074 | ); 1075 | progressHandler(100); 1076 | await fetch(`/api/metadata`, { 1077 | method: "POST", 1078 | body: JSON.stringify(metadata), 1079 | }); 1080 | progressHandler(100); 1081 | showSnack(`Uploaded ${file.name}`, COLOR_BLUE, "success"); 1082 | updateSpaceUsage(file.size); 1083 | if (!refreshList) { 1084 | return; 1085 | } 1086 | openedFolderGL 1087 | ? handleFolderClick(openedFolderGL) 1088 | : currentOption().click(); 1089 | hideRightNav(); 1090 | } else { 1091 | taskFactoryGL[hash].bar.style.backgroundColor = COLOR_RED; 1092 | showSnack(`Failed to upload ${file.name}`, COLOR_RED, "error"); 1093 | await fetch( 1094 | `${ROOT}/${projectId}/filebox/uploads/${uploadId}?name=${name}`, 1095 | { 1096 | method: "DELETE", 1097 | headers: header, 1098 | } 1099 | ); 1100 | } 1101 | } 1102 | }; 1103 | reader.readAsArrayBuffer(file); 1104 | } 1105 | 1106 | async function fetchFileFromDrive(file, progressHandler) { 1107 | progressHandler(0); 1108 | let header = { "X-Api-Key": secretKeyGL }; 1109 | let projectId = secretKeyGL.split("_")[0]; 1110 | const ROOT = "https://drive.deta.sh/v1"; 1111 | let extension = file.name.split(".").pop(); 1112 | let qualifiedName = file.hash + "." + extension; 1113 | return fetch( 1114 | `${ROOT}/${projectId}/filebox/files/download?name=${qualifiedName}`, 1115 | { 1116 | method: "GET", 1117 | headers: header, 1118 | } 1119 | ) 1120 | .then((response) => { 1121 | const reader = response.body.getReader(); 1122 | return new ReadableStream({ 1123 | start(controller) { 1124 | return pump(); 1125 | function pump() { 1126 | return reader.read().then(({ done, value }) => { 1127 | if (done) { 1128 | controller.close(); 1129 | return; 1130 | } 1131 | controller.enqueue(value); 1132 | progressHandler(value.length); 1133 | return pump(); 1134 | }); 1135 | } 1136 | }, 1137 | }); 1138 | }) 1139 | .then((stream) => new Response(stream)) 1140 | .then((response) => response.blob()); 1141 | } 1142 | 1143 | async function download(file, progressHandler) { 1144 | showSnack(`Downloading ${file.name}`, COLOR_GREEN, "info"); 1145 | progressHandler(0); 1146 | queueButton.click(); 1147 | let header = { "X-Api-Key": secretKeyGL }; 1148 | let projectId = secretKeyGL.split("_")[0]; 1149 | const ROOT = "https://drive.deta.sh/v1"; 1150 | let extension = file.name.split(".").pop(); 1151 | let qualifiedName = file.hash + "." + extension; 1152 | let resp = await fetch( 1153 | `${ROOT}/${projectId}/filebox/files/download?name=${qualifiedName}`, 1154 | { 1155 | method: "GET", 1156 | headers: header, 1157 | } 1158 | ); 1159 | let progress = 0; 1160 | const reader = resp.body.getReader(); 1161 | let stream = new ReadableStream({ 1162 | start(controller) { 1163 | return pump(); 1164 | function pump() { 1165 | return reader.read().then(({ done, value }) => { 1166 | if (done) { 1167 | controller.close(); 1168 | return; 1169 | } 1170 | controller.enqueue(value); 1171 | progress += value.length; 1172 | progressHandler(Math.round((progress / file.size) * 100)); 1173 | return pump(); 1174 | }); 1175 | } 1176 | }, 1177 | }); 1178 | let blob = await new Response(stream).blob(); 1179 | let a = document.createElement("a"); 1180 | a.href = URL.createObjectURL(blob); 1181 | a.download = file.name; 1182 | showSnack(`Downloaded ${file.name}`, COLOR_GREEN, "success"); 1183 | hideRightNav(); 1184 | a.click(); 1185 | } 1186 | 1187 | function downloadShared(file, progressHandler) { 1188 | showSnack(`Downloading ${file.name}`, COLOR_GREEN, "info"); 1189 | progressHandler(0); 1190 | queueButton.click(); 1191 | let size = file.size; 1192 | const chunkSize = 1024 * 1024 * 4; 1193 | if (size < chunkSize) { 1194 | fetch(`/api/external/${userIdGL}/${file.owner}/${file.hash}/0`) 1195 | .then((resp) => resp.blob()) 1196 | .then((blob) => { 1197 | let a = document.createElement("a"); 1198 | a.href = URL.createObjectURL(blob); 1199 | a.download = file.name; 1200 | progressHandler(100); 1201 | showSnack(`Downloaded ${file.name}`, COLOR_GREEN, "success"); 1202 | hideRightNav(); 1203 | a.click(); 1204 | }); 1205 | } else { 1206 | let skips = 0; 1207 | if (size % chunkSize === 0) { 1208 | skips = size / chunkSize; 1209 | } else { 1210 | skips = Math.floor(size / chunkSize) + 1; 1211 | } 1212 | let heads = Array.from(Array(skips).keys()); 1213 | let promises = []; 1214 | let progress = 0; 1215 | heads.forEach((head) => { 1216 | promises.push( 1217 | fetch(`/api/external/${userIdGL}/${file.owner}/${file.hash}/${head}`) 1218 | .then((resp) => { 1219 | return resp.blob(); 1220 | }) 1221 | .then((blob) => { 1222 | progress += blob.size; 1223 | progressHandler(Math.round((progress / file.size) * 100)); 1224 | return blob; 1225 | }) 1226 | ); 1227 | }); 1228 | Promise.all(promises).then((blobs) => { 1229 | progressHandler(100); 1230 | let a = document.createElement("a"); 1231 | a.href = URL.createObjectURL(new Blob(blobs, { type: file.mime })); 1232 | a.download = file.name; 1233 | showSnack(`Downloaded ${file.name}`, COLOR_GREEN, "success"); 1234 | hideRightNav(); 1235 | a.click(); 1236 | }); 1237 | } 1238 | } 1239 | 1240 | /////////////////////// Basic Utils /////////////////////// 1241 | 1242 | function renderInRightNav(elem) { 1243 | navRight.innerHTML = ""; 1244 | navRight.appendChild(elem); 1245 | navRight.style.display = "flex"; 1246 | blurLayer.style.display = "block"; 1247 | } 1248 | 1249 | function hideRightNav() { 1250 | navRight.style.display = "none"; 1251 | blurLayer.style.display = "none"; 1252 | } 1253 | 1254 | function dateStringToTimestamp(dateString) { 1255 | let date = new Date(dateString); 1256 | return date.getTime(); 1257 | } 1258 | 1259 | function sortFileByTimestamp(data) { 1260 | data = data.sort((a, b) => { 1261 | return dateStringToTimestamp(b.date) - dateStringToTimestamp(a.date); 1262 | }); 1263 | return data; 1264 | } 1265 | 1266 | function handleSizeUnit(size) { 1267 | if (size === undefined) { 1268 | return "~"; 1269 | } 1270 | if (size < 1024) { 1271 | return size + " B"; 1272 | } else if (size < 1024 * 1024) { 1273 | return (size / 1024).toFixed(2) + " KB"; 1274 | } else if (size < 1024 * 1024 * 1024) { 1275 | return (size / 1024 / 1024).toFixed(2) + " MB"; 1276 | } else { 1277 | return (size / 1024 / 1024 / 1024).toFixed(2) + " GB"; 1278 | } 1279 | } 1280 | 1281 | function formatDateString(date) { 1282 | date = new Date(date); 1283 | return ` 1284 | ${date.toLocaleString("default", { month: "short" })} 1285 | ${date.getDate()}, 1286 | ${date.getFullYear()} 1287 | ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()} 1288 | `; 1289 | } 1290 | 1291 | function updateSpaceUsage(incr) { 1292 | usageGL += incr; 1293 | usageElem.innerText = `${handleSizeUnit(usageGL)}`; 1294 | } 1295 | 1296 | function setIconByMime(mime, elem) { 1297 | if (mime === undefined) { 1298 | elem.innerHTML = `folder`; 1299 | } else if (mime.startsWith("image")) { 1300 | elem.innerHTML = `image`; 1301 | } else if (mime.startsWith("video")) { 1302 | elem.innerHTML = `movie`; 1303 | } else if (mime.startsWith("audio")) { 1304 | elem.innerHTML = `music_note`; 1305 | } else if (mime.startsWith("text")) { 1306 | elem.innerHTML = `text_snippet`; 1307 | } else if (mime.startsWith("application/pdf")) { 1308 | elem.innerHTML = `book`; 1309 | } else if (mime.startsWith("application/zip")) { 1310 | elem.innerHTML = `archive`; 1311 | } else if (mime.startsWith("application/x-rar-compressed")) { 1312 | elem.innerHTML = `archive`; 1313 | } else if (mime.startsWith("font")) { 1314 | elem.innerHTML = `format_size`; 1315 | } else { 1316 | elem.innerHTML = `draft`; 1317 | } 1318 | } 1319 | 1320 | function randId() { 1321 | return crypto.randomUUID().replace(/-/g, ""); 1322 | } 1323 | 1324 | function buildFileMetadata(file) { 1325 | let hash = randId(); 1326 | let meta = { 1327 | hash: hash, 1328 | name: file.name, 1329 | size: file.size, 1330 | mime: file.type, 1331 | access: "private", 1332 | date: new Date().toISOString(), 1333 | }; 1334 | if (openedFolderGL) { 1335 | meta.parent = openedFolderGL.parent 1336 | ? `${openedFolderGL.parent}/${openedFolderGL.name}` 1337 | : openedFolderGL.name; 1338 | } else { 1339 | meta.parent = null; 1340 | } 1341 | return meta; 1342 | } 1343 | 1344 | function progressHandlerById(hash, percentage) { 1345 | taskFactoryGL[hash].percentage.innerHTML = `${percentage}%`; 1346 | taskFactoryGL[hash].bar.style.width = `${percentage}%`; 1347 | } 1348 | 1349 | async function checkFileParentExists(file) { 1350 | if (!file.parent) { 1351 | return false; 1352 | } 1353 | let body = { type: "folder" }; 1354 | let fragments = file.parent.split("/"); 1355 | if (fragments.length === 1) { 1356 | body["name"] = file.parent; 1357 | } else { 1358 | body["name"] = fragments[fragments.length - 1]; 1359 | body["parent"] = fragments.slice(0, fragments.length - 1).join("/"); 1360 | } 1361 | let resp = await fetch(`/api/query`, { 1362 | method: "POST", 1363 | body: JSON.stringify(body), 1364 | }); 1365 | let data = await resp.json(); 1366 | if (!data) { 1367 | return false; 1368 | } 1369 | return true; 1370 | } 1371 | 1372 | async function createFolder() { 1373 | let name = prompt("Enter folder name", "New Folder"); 1374 | if (name === "") { 1375 | showSnack(`Folder name cannot be empty`, COLOR_ORANGE, "warning"); 1376 | return; 1377 | } 1378 | if (name === "~shared") { 1379 | showSnack(`~shared is a reserved folder name`, COLOR_ORANGE, "warning"); 1380 | return; 1381 | } 1382 | if (name && name.includes("/")) { 1383 | showSnack(`Folder name cannot contain /`, COLOR_ORANGE, "warning"); 1384 | return; 1385 | } 1386 | if (!name) { 1387 | return; 1388 | } 1389 | let body = { 1390 | name: name, 1391 | type: "folder", 1392 | hash: randId(), 1393 | date: new Date().toISOString(), 1394 | parent: null, 1395 | }; 1396 | if (openedFolderGL) { 1397 | body.parent = openedFolderGL.parent 1398 | ? `${openedFolderGL.parent}/${openedFolderGL.name}` 1399 | : openedFolderGL.name; 1400 | } 1401 | let resp = await fetch(`/api/metadata`, { 1402 | method: "POST", 1403 | body: JSON.stringify(body), 1404 | }); 1405 | if (resp.status === 409) { 1406 | showSnack(`Folder with same name already exists`, COLOR_RED, "error"); 1407 | return; 1408 | } else if (resp.status <= 207) { 1409 | showSnack(`Created folder ${name}`, COLOR_GREEN, "success"); 1410 | handleFolderClick(body); 1411 | } 1412 | } 1413 | 1414 | async function buildChildrenTree(folder) { 1415 | let node = []; 1416 | let resp = await fetch(`/api/query`, { 1417 | method: "POST", 1418 | body: JSON.stringify({ 1419 | "deleted?ne": true, 1420 | "shared?ne": true, 1421 | parent: folder.parent ? `${folder.parent}/${folder.name}` : folder.name, 1422 | }), 1423 | }); 1424 | let children = await resp.json(); 1425 | if (!children) { 1426 | return { tree: node, hash: folder.hash }; 1427 | } 1428 | let promises = []; 1429 | children.forEach((child) => { 1430 | if (child.type === "folder") { 1431 | promises.push(buildChildrenTree(child)); 1432 | node.push(child); 1433 | } else { 1434 | node.push(child); 1435 | } 1436 | }); 1437 | let childrenTree = await Promise.all(promises); 1438 | childrenTree.forEach((childTree) => { 1439 | node.forEach((child) => { 1440 | if (child.hash === childTree.hash) { 1441 | child.children = childTree.tree; 1442 | } 1443 | }); 1444 | }); 1445 | return { tree: node, hash: folder.hash }; 1446 | } 1447 | 1448 | function caculateTreeSize(tree) { 1449 | let size = 0; 1450 | tree.forEach((child) => { 1451 | if (child.type === "folder") { 1452 | size += caculateTreeSize(child.children); 1453 | } else { 1454 | size += child.size; 1455 | } 1456 | }); 1457 | return size; 1458 | } 1459 | 1460 | async function zipFolderRecursive(tree, hash, zip, done, totalSize) { 1461 | let promises = []; 1462 | let recursions = []; 1463 | tree.forEach((child) => { 1464 | if (child.type === "folder") { 1465 | let folder = zip.folder(child.name); 1466 | if (child.children.length > 0) { 1467 | recursions.push( 1468 | zipFolderRecursive(child.children, hash, folder, done, totalSize) 1469 | ); 1470 | } 1471 | } else { 1472 | promises.push( 1473 | fetchFileFromDrive(child, (progress) => { 1474 | done += progress; 1475 | let percentage = Math.round((done / totalSize) * 100); 1476 | progressHandlerById(hash, percentage); 1477 | }) 1478 | ); 1479 | } 1480 | }); 1481 | await Promise.all(recursions); 1482 | let blobs = await Promise.all(promises); 1483 | blobs.forEach((blob, index) => { 1484 | zip.file(tree[index].name, blob); 1485 | }); 1486 | progressHandlerById(hash, 100); 1487 | return zip; 1488 | } 1489 | 1490 | /////////////////////// Dynamic Renderer /////////////////////// 1491 | 1492 | // function sendNotification(body, tag = 'filebox') { 1493 | // let enabled = Notification.permission === 'granted'; 1494 | // if (!enabled) { 1495 | // return; 1496 | // } 1497 | // new Notification("Filebox", { 1498 | // body: body, 1499 | // tag: tag || 'filebox', 1500 | // icon: '/assets/icon.png', 1501 | // }); 1502 | // } 1503 | 1504 | async function updateFolderStats(folders) { 1505 | if (folders.length === 0) { 1506 | return; 1507 | } 1508 | let resp = await fetch(`/api/count/items`, { 1509 | method: "POST", 1510 | body: JSON.stringify(folders), 1511 | }); 1512 | let stat = await resp.json(); 1513 | stat.forEach((stat) => { 1514 | let statElem = document.getElementById(`stat-${stat.hash}`); 1515 | if (statElem) { 1516 | statElem.innerHTML = `${stat.count} items • ${statElem.innerHTML}`; 1517 | } 1518 | }); 1519 | } 1520 | 1521 | async function downloadFolderAsZip(folder) { 1522 | const { tree, _ } = await buildChildrenTree(folder); 1523 | let totalSize = caculateTreeSize(tree); 1524 | let folderData = { 1525 | name: folder.name, 1526 | size: totalSize, 1527 | type: "folder", 1528 | hash: folder.hash, 1529 | }; 1530 | prependQueueElem(folderData, false); 1531 | showSnack(`Zipping ${folder.name}...`, COLOR_BLUE, `info`); 1532 | const zip = await zipFolderRecursive( 1533 | tree, 1534 | folder.hash, 1535 | new JSZip(), 1536 | 0, 1537 | totalSize 1538 | ); 1539 | let content = await zip.generateAsync({ type: "blob" }); 1540 | let a = document.createElement("a"); 1541 | a.href = window.URL.createObjectURL(content); 1542 | a.download = `${folder.name}.zip`; 1543 | a.click(); 1544 | } 1545 | 1546 | async function deleteFolderPermanently(folder) { 1547 | let ok = confirm( 1548 | `Are you sure you want to delete folder "${folder.name}" permanently?` 1549 | ); 1550 | if (!ok) return; 1551 | const { tree, _ } = await buildChildrenTree(folder); 1552 | let treeSize = caculateTreeSize(tree); 1553 | async function deleteFilesRecursively(tree) { 1554 | await fetch(`/api/metadata`, { 1555 | method: "DELETE", 1556 | body: JSON.stringify(folder), 1557 | }); 1558 | let folderElem = document.getElementById(`file-${folder.hash}`); 1559 | if (folderElem) { 1560 | folderElem.remove(); 1561 | } 1562 | tree.forEach(async (file) => { 1563 | if (file.type === "folder") { 1564 | await deleteFilesRecursively(file.children); 1565 | } 1566 | await fetch(`/api/metadata`, { 1567 | method: "DELETE", 1568 | body: JSON.stringify(file), 1569 | }); 1570 | }); 1571 | } 1572 | await deleteFilesRecursively(tree); 1573 | updateSpaceUsage(-treeSize); 1574 | } 1575 | 1576 | function handleTrashFileMenuClick(file) { 1577 | let fileOptionPanel = document.createElement("div"); 1578 | fileOptionPanel.className = "file_menu"; 1579 | let title = document.createElement("div"); 1580 | title.className = "title"; 1581 | let fileNameElem = document.createElement("p"); 1582 | fileNameElem.innerHTML = file.name; 1583 | title.appendChild(fileNameElem); 1584 | let close = document.createElement("span"); 1585 | close.className = `material-symbols-rounded`; 1586 | close.innerHTML = `chevron_right`; 1587 | close.addEventListener("click", () => { 1588 | hideRightNav(); 1589 | }); 1590 | title.appendChild(close); 1591 | fileOptionPanel.appendChild(title); 1592 | let restore = document.createElement("div"); 1593 | restore.className = "file_menu_option"; 1594 | restore.innerHTML = `Restore
replay`; 1595 | restore.addEventListener("click", async () => { 1596 | delete file.deleted; 1597 | let ok = await checkFileParentExists(file); 1598 | if (!ok && file.parent !== undefined) { 1599 | showSnack( 1600 | `Parent not found. Restoring to root`, 1601 | COLOR_ORANGE, 1602 | "warning" 1603 | ); 1604 | file.parent = null; 1605 | } 1606 | await fetch(`/api/metadata`, { 1607 | method: "PUT", 1608 | body: JSON.stringify(file), 1609 | }); 1610 | showSnack(`Restored ${file.name}`, COLOR_GREEN, "success"); 1611 | document.getElementById(`file-${file.hash}`).remove(); 1612 | delete trashFilesGL[file.hash]; 1613 | close.click(); 1614 | }); 1615 | let deleteButton = document.createElement("div"); 1616 | deleteButton.className = "file_menu_option"; 1617 | deleteButton.innerHTML = `Delete Permanently
delete_forever`; 1618 | deleteButton.addEventListener("click", () => { 1619 | fetch(`/api/metadata`, { 1620 | method: "DELETE", 1621 | body: JSON.stringify(file), 1622 | }).then(() => { 1623 | showSnack(`Permanently deleted ${file.name}`, COLOR_RED, "info"); 1624 | document.getElementById(`file-${file.hash}`).remove(); 1625 | if (!file.shared) { 1626 | updateSpaceUsage(-file.size); 1627 | } 1628 | close.click(); 1629 | if (trashFilesGL.length === 0) { 1630 | navTop.removeChild(navTop.firstChild); 1631 | } 1632 | }); 1633 | }); 1634 | fileOptionPanel.appendChild(restore); 1635 | fileOptionPanel.appendChild(deleteButton); 1636 | renderInRightNav(fileOptionPanel); 1637 | } 1638 | 1639 | function handleFileMenuClick(file) { 1640 | let fileOptionPanel = document.createElement("div"); 1641 | fileOptionPanel.className = "file_menu"; 1642 | 1643 | // Title 1644 | let title = document.createElement("div"); 1645 | title.className = "title"; 1646 | let fileNameElem = document.createElement("p"); 1647 | fileNameElem.innerHTML = file.name; 1648 | title.appendChild(fileNameElem); 1649 | let close = document.createElement("span"); 1650 | close.className = `material-symbols-rounded`; 1651 | close.innerHTML = `chevron_right`; 1652 | close.addEventListener("click", () => { 1653 | hideRightNav(); 1654 | }); 1655 | title.appendChild(close); 1656 | fileOptionPanel.appendChild(title); 1657 | 1658 | // Access 1659 | let visibilityOption = document.createElement("div"); 1660 | visibilityOption.className = "file_menu_option"; 1661 | let visibility = file.access === "private" ? "visibility_off" : "visibility"; 1662 | visibilityOption.innerHTML = `Access
${visibility}`; 1663 | visibilityOption.addEventListener("click", () => { 1664 | if (file.access === "private") { 1665 | file.access = "public"; 1666 | visibilityOption.innerHTML = `Access
visibility`; 1667 | share.style.opacity = 1; 1668 | file.size > 1024 * 1024 * 4 ? (embed.style.opacity = 0.3) : (embed.style.opacity = 1); 1669 | showSnack("Access changed to public", COLOR_GREEN, "info"); 1670 | } else { 1671 | file.access = "private"; 1672 | visibilityOption.innerHTML = `Access
visibility_off`; 1673 | share.style.opacity = 0.3; 1674 | embed.style.opacity = 0.3; 1675 | showSnack("Access changed to private", COLOR_ORANGE, "info"); 1676 | } 1677 | fetch(`/api/metadata`, { 1678 | method: "PATCH", 1679 | body: JSON.stringify({ hash: file.hash, access: file.access }), 1680 | }); 1681 | }); 1682 | if (file.type !== "folder") { 1683 | fileOptionPanel.appendChild(visibilityOption); 1684 | } 1685 | 1686 | // Bookmark 1687 | let bookmarkMode = file.pinned ? "remove" : "add"; 1688 | let bookmarkOption = document.createElement("div"); 1689 | bookmarkOption.className = "file_menu_option"; 1690 | bookmarkOption.innerHTML = `Pin
${bookmarkMode}`; 1691 | bookmarkOption.addEventListener("click", () => { 1692 | if (file.pinned) { 1693 | fetch(`/api/metadata`, { 1694 | method: "PATCH", 1695 | body: JSON.stringify({ hash: file.hash, pinned: false }), 1696 | }).then(() => { 1697 | showSnack(`Unpinned successfully`, COLOR_ORANGE, "info"); 1698 | let card = document.getElementById(`card-${file.hash}`); 1699 | if (card) { 1700 | card.remove(); 1701 | } 1702 | delete file.pinned; 1703 | bookmarkOption.innerHTML = `Pin
add`; 1704 | }); 1705 | } else { 1706 | fetch(`/api/metadata`, { 1707 | method: "PATCH", 1708 | body: JSON.stringify({ hash: file.hash, pinned: true }), 1709 | }).then(() => { 1710 | showSnack(`Pinned successfully`, COLOR_GREEN, "success"); 1711 | let pins = document.querySelector(".pinned_files"); 1712 | if (pins) { 1713 | pins.appendChild(newFileElem(file)); 1714 | } 1715 | file.pinned = true; 1716 | bookmarkOption.innerHTML = `Pin
remove`; 1717 | }); 1718 | } 1719 | }); 1720 | fileOptionPanel.appendChild(bookmarkOption); 1721 | 1722 | // Share 1723 | let send = document.createElement("div"); 1724 | send.className = "file_menu_option"; 1725 | send.innerHTML = `Send
send`; 1726 | if (file.type !== "folder") { 1727 | send.addEventListener("click", () => { 1728 | if (file.owner) { 1729 | showSnack("Can't send a file that you don't own", COLOR_ORANGE, "info"); 1730 | return; 1731 | } 1732 | renderFileSenderModal(file); 1733 | }); 1734 | fileOptionPanel.appendChild(send); 1735 | } 1736 | 1737 | // Rename 1738 | let rename = document.createElement("div"); 1739 | rename.className = "file_menu_option"; 1740 | rename.innerHTML = `Rename
edit`; 1741 | rename.addEventListener("click", () => { 1742 | showRenameModal(file); 1743 | }); 1744 | 1745 | // Download 1746 | let downloadButton = document.createElement("div"); 1747 | downloadButton.className = "file_menu_option"; 1748 | downloadButton.innerHTML = `Download
download`; 1749 | downloadButton.addEventListener("click", () => { 1750 | close.click(); 1751 | prependQueueElem(file, false); 1752 | if (file.shared === true) { 1753 | downloadShared(file, (percentage) => { 1754 | progressHandlerById(file.hash, percentage); 1755 | }); 1756 | return; 1757 | } 1758 | download(file, (percentage) => { 1759 | progressHandlerById(file.hash, percentage); 1760 | }); 1761 | }); 1762 | 1763 | // Share 1764 | let share = document.createElement("div"); 1765 | share.className = "file_menu_option"; 1766 | share.innerHTML = `Share Link
link`; 1767 | share.addEventListener("click", () => { 1768 | if (file.access === "private") { 1769 | showSnack(`Make file public to share via link`, COLOR_ORANGE, "warning"); 1770 | } else { 1771 | window.navigator.clipboard 1772 | .writeText(`${window.location.origin}/shared/${file.hash}`) 1773 | .then(() => { 1774 | showSnack(`Copied to clipboard`, COLOR_GREEN, "success"); 1775 | }); 1776 | } 1777 | }); 1778 | 1779 | // Embed 1780 | let embed = document.createElement("div"); 1781 | embed.className = "file_menu_option"; 1782 | embed.innerHTML = `Embed
code`; 1783 | embed.addEventListener("click", () => { 1784 | if (file.access === "private") { 1785 | showSnack(`Make file public to embed`, COLOR_ORANGE, "warning"); 1786 | } else if (file.size > 1024 * 1024 * 4) { 1787 | showSnack(`File is too large to embed`, COLOR_RED, "error"); 1788 | } else { 1789 | window.navigator.clipboard 1790 | .writeText(`${window.location.origin}/embed/${file.hash}`) 1791 | .then(() => { 1792 | showSnack(`Copied to clipboard`, COLOR_GREEN, "success"); 1793 | }); 1794 | } 1795 | }); 1796 | 1797 | // Move 1798 | let move = document.createElement("div"); 1799 | move.className = "file_menu_option"; 1800 | move.innerHTML = `Move
arrow_forward`; 1801 | move.addEventListener("click", () => { 1802 | close.click(); 1803 | renderAuxNav(fileMover(file)); 1804 | isFileMovingGL = true; 1805 | browseButton.click(); 1806 | }); 1807 | if (file.type !== "folder") { 1808 | fileOptionPanel.appendChild(rename); 1809 | fileOptionPanel.appendChild(downloadButton); 1810 | if (file.access === "private") { 1811 | share.style.opacity = 0.3; 1812 | } 1813 | if (file.access === "private" || file.size > 1024 * 1024 * 4) { 1814 | embed.style.opacity = 0.3; 1815 | } 1816 | fileOptionPanel.appendChild(embed); 1817 | fileOptionPanel.appendChild(move); 1818 | } 1819 | fileOptionPanel.appendChild(share); 1820 | 1821 | // Download as zip 1822 | let downloadZip = document.createElement("div"); 1823 | downloadZip.className = "file_menu_option"; 1824 | downloadZip.innerHTML = `Download as Zip
archive`; 1825 | downloadZip.addEventListener("click", () => { 1826 | downloadFolderAsZip(file); 1827 | }); 1828 | if (file.type === "folder") { 1829 | fileOptionPanel.appendChild(downloadZip); 1830 | } 1831 | 1832 | // Trash 1833 | let trashButton = document.createElement("div"); 1834 | trashButton.className = "file_menu_option"; 1835 | if (file.type === "folder") { 1836 | trashButton.innerHTML = `Delete Permanently
delete_forever`; 1837 | } else { 1838 | trashButton.innerHTML = `Trash
delete_forever`; 1839 | } 1840 | trashButton.addEventListener("click", async () => { 1841 | if (file.type === "folder") { 1842 | deleteFolderPermanently(file).then(() => { 1843 | showSnack( 1844 | `Deleted folder "${file.name}" permanently`, 1845 | COLOR_RED, 1846 | "warning" 1847 | ); 1848 | close.click(); 1849 | }); 1850 | } else { 1851 | file.deleted = true; 1852 | fetch(`/api/metadata`, { 1853 | method: "PUT", 1854 | body: JSON.stringify(file), 1855 | }).then(() => { 1856 | showSnack(`Moved to trash ${file.name}`, COLOR_RED, "warning"); 1857 | document.getElementById(`file-${file.hash}`).remove(); 1858 | close.click(); 1859 | }); 1860 | } 1861 | }); 1862 | fileOptionPanel.appendChild(trashButton); 1863 | 1864 | // Access Control 1865 | if (file.recipients && file.recipients.length > 0) { 1866 | let p = document.createElement("p"); 1867 | p.innerText = "Block Access"; 1868 | p.style.fontSize = "14px"; 1869 | p.style.color = "white"; 1870 | p.style.width = "100%"; 1871 | p.style.padding = "10px 20px"; 1872 | p.style.backgroundColor = "rgba(242, 58, 58, 0.5)"; 1873 | fileOptionPanel.appendChild(p); 1874 | file.recipients.forEach((recipient) => { 1875 | let recipientElem = document.createElement("div"); 1876 | recipientElem.className = "file_menu_option"; 1877 | recipientElem.innerHTML = `${recipient}
block`; 1878 | recipientElem.addEventListener("click", () => { 1879 | file.recipients = file.recipients.filter((r) => r !== recipient); 1880 | fetch(`/api/metadata`, { 1881 | method: "PUT", 1882 | body: JSON.stringify(file), 1883 | }).then(() => { 1884 | showSnack(`Blocked access for ${recipient}`, COLOR_ORANGE, "warning"); 1885 | recipientElem.remove(); 1886 | }); 1887 | }); 1888 | fileOptionPanel.appendChild(recipientElem); 1889 | }); 1890 | } 1891 | 1892 | renderInRightNav(fileOptionPanel); 1893 | } 1894 | 1895 | async function handleFolderClick(folder) { 1896 | openedFolderGL = folder; 1897 | let parentOf = folder.parent 1898 | ? `${folder.parent}/${folder.name}` 1899 | : folder.name; 1900 | const resp = await fetch(`/api/query`, { 1901 | method: "POST", 1902 | body: JSON.stringify({ "parent": parentOf, "deleted?ne": true}), 1903 | }) 1904 | const data = await resp.json() 1905 | view.innerHTML = ""; 1906 | view.appendChild(buildPrompt(folder)); 1907 | let fragment = parentOf ? `~/${parentOf}` : "home"; 1908 | document.querySelector(".fragment").innerText = fragment; 1909 | if (!data) return; 1910 | let folders = []; 1911 | let files = []; 1912 | data.forEach((file) => { 1913 | file.type === "folder" ? folders.push(file) : files.push(file); 1914 | }); 1915 | let list = document.createElement("ul"); 1916 | view.appendChild(list); 1917 | folders.concat(files).forEach((file) => { 1918 | list.appendChild(newFileElem(file)); 1919 | }); 1920 | updateFolderStats(folders); 1921 | } 1922 | 1923 | function handleMultiSelectMenuClick() { 1924 | let fileOptionPanel = document.createElement("div"); 1925 | fileOptionPanel.className = "file_menu"; 1926 | 1927 | // Title 1928 | let title = document.createElement("div"); 1929 | title.className = "title"; 1930 | let fileNameElem = document.createElement("p"); 1931 | fileNameElem.innerHTML = "Options"; 1932 | title.appendChild(fileNameElem); 1933 | let close = document.createElement("span"); 1934 | close.className = `material-symbols-rounded`; 1935 | close.innerHTML = `chevron_right`; 1936 | close.addEventListener("click", () => { 1937 | hideRightNav(); 1938 | }); 1939 | title.appendChild(close); 1940 | fileOptionPanel.appendChild(title); 1941 | 1942 | // Zip Option 1943 | let zipOption = document.createElement("div"); 1944 | zipOption.className = "file_menu_option"; 1945 | zipOption.innerHTML = `Download as Zip
archive`; 1946 | zipOption.addEventListener("click", () => { 1947 | let zip = new JSZip(); 1948 | let totalSize = 0; 1949 | multiSelectBucketGL.forEach((file) => { 1950 | totalSize += parseInt(file.size); 1951 | }); 1952 | let randomZipId = randId(); 1953 | let zipData = { 1954 | name: `filebox-download-${randomZipId}.zip`, 1955 | mime: "application/zip", 1956 | size: totalSize, 1957 | hash: randomZipId, 1958 | }; 1959 | prependQueueElem(zipData); 1960 | queueButton.click(); 1961 | let promises = []; 1962 | let completed = 0; 1963 | multiSelectBucketGL.forEach((file) => { 1964 | promises.push( 1965 | fetchFileFromDrive(file, (cmp) => { 1966 | completed += cmp; 1967 | let percentage = Math.round((completed / totalSize) * 100); 1968 | progressHandlerById(zipData.hash, percentage); 1969 | }).then((blob) => { 1970 | zip.file(file.name, new Blob([blob], { type: file.mime })); 1971 | }) 1972 | ); 1973 | }); 1974 | Promise.all(promises).then(() => { 1975 | zip.generateAsync({ type: "blob" }).then((content) => { 1976 | let a = document.createElement("a"); 1977 | a.href = window.URL.createObjectURL(content); 1978 | a.download = zipData.name; 1979 | a.click(); 1980 | }); 1981 | }); 1982 | }) 1983 | 1984 | // Move Option 1985 | let moveOption = document.createElement("div"); 1986 | moveOption.className = "file_menu_option"; 1987 | moveOption.innerHTML = `Move
arrow_forward`; 1988 | moveOption.addEventListener("click", () => { 1989 | isFileMovingGL = true; 1990 | browseButton.click(); 1991 | let fileMover = document.createElement("div"); 1992 | fileMover.className = "file_mover"; 1993 | let cancelButton = document.createElement("button"); 1994 | cancelButton.innerHTML = "Cancel"; 1995 | cancelButton.addEventListener("click", () => { 1996 | isFileMovingGL = false; 1997 | multiSelectBucketGL = []; 1998 | navTop.removeChild(navTop.firstChild); 1999 | }); 2000 | let selectButton = document.createElement("button"); 2001 | selectButton.innerHTML = "Select"; 2002 | selectButton.style.backgroundColor = "var(--accent-blue)"; 2003 | selectButton.addEventListener("click", () => { 2004 | multiSelectBucketGL.forEach((file) => { 2005 | delete file.deleted; 2006 | }); 2007 | if (!openedFolderGL) { 2008 | multiSelectBucketGL.forEach((file) => { 2009 | delete file.parent; 2010 | }); 2011 | } else { 2012 | multiSelectBucketGL.forEach((file) => { 2013 | if (openedFolderGL.parent) { 2014 | file.parent = `${openedFolderGL.parent}/${openedFolderGL.name}`; 2015 | } else { 2016 | file.parent = openedFolderGL.name; 2017 | } 2018 | }); 2019 | } 2020 | fetch(`/api/bulk`, { 2021 | method: "PATCH", 2022 | body: JSON.stringify(multiSelectBucketGL), 2023 | }).then(() => { 2024 | showSnack("Files Moved Successfully", COLOR_GREEN, "success"); 2025 | if (openedFolderGL) { 2026 | handleFolderClick(openedFolderGL); 2027 | } else { 2028 | browseButton.click(); 2029 | } 2030 | navTop.removeChild(navTop.firstChild); 2031 | }); 2032 | }); 2033 | let p = document.createElement("p"); 2034 | p.innerHTML = "Select Move Destination"; 2035 | fileMover.appendChild(cancelButton); 2036 | fileMover.appendChild(p); 2037 | fileMover.appendChild(selectButton); 2038 | navTop.removeChild(navTop.firstChild); 2039 | renderAuxNav(fileMover); 2040 | }) 2041 | 2042 | // Private Option 2043 | let privateOption = document.createElement("div"); 2044 | privateOption.className = "file_menu_option"; 2045 | privateOption.innerHTML = `Make Private
visibility_off`; 2046 | privateOption.addEventListener("click", () => { 2047 | multiSelectBucketGL.forEach((file) => { 2048 | file.access = "private"; 2049 | }); 2050 | fetch(`/api/bulk`, { 2051 | method: "PATCH", 2052 | body: JSON.stringify(multiSelectBucketGL), 2053 | }).then(() => { 2054 | showSnack(`Made selected files private`, COLOR_ORANGE, "info"); 2055 | }); 2056 | }) 2057 | 2058 | // Public Option 2059 | let publicOption = document.createElement("div"); 2060 | publicOption.className = "file_menu_option"; 2061 | publicOption.innerHTML = `Make Public
visibility`; 2062 | publicOption.addEventListener("click", () => { 2063 | multiSelectBucketGL.forEach((file) => { 2064 | file.access = "public"; 2065 | }); 2066 | fetch(`/api/bulk`, { 2067 | method: "PATCH", 2068 | body: JSON.stringify(multiSelectBucketGL), 2069 | }).then(() => { 2070 | showSnack(`Made selected files public`, COLOR_ORANGE, "info"); 2071 | }); 2072 | }) 2073 | 2074 | // Delete Option 2075 | let deleteOption = document.createElement("div"); 2076 | deleteOption.className = "file_menu_option"; 2077 | deleteOption.innerHTML = `Delete
delete_forever`; 2078 | deleteOption.addEventListener("click", () => { 2079 | let ok = confirm( 2080 | `Do you really want to delete ${multiSelectBucketGL.length} file(s)?` 2081 | ); 2082 | if (!ok) { 2083 | return; 2084 | } 2085 | fetch(`/api/bulk`, { 2086 | method: "DELETE", 2087 | body: JSON.stringify(multiSelectBucketGL), 2088 | }).then(() => { 2089 | multiSelectBucketGL.forEach((file) => { 2090 | document.getElementById(`file-${file.hash}`).remove(); 2091 | }); 2092 | multiSelectBucketGL = []; 2093 | navTop.removeChild(navTop.firstChild); 2094 | showSnack(`Deleted selected files`, COLOR_RED, "info"); 2095 | }); 2096 | }) 2097 | 2098 | fileOptionPanel.appendChild(zipOption); 2099 | fileOptionPanel.appendChild(moveOption); 2100 | fileOptionPanel.appendChild(privateOption); 2101 | fileOptionPanel.appendChild(publicOption); 2102 | fileOptionPanel.appendChild(deleteOption); 2103 | 2104 | renderInRightNav(fileOptionPanel); 2105 | 2106 | } 2107 | 2108 | function newFileElem(file, trashed = false) { 2109 | let li = document.createElement("li"); 2110 | li.dataset.parent = file.parent || ""; 2111 | li.dataset.name = file.name; 2112 | li.dataset.hash = file.hash; 2113 | if (file.type === "folder") { 2114 | li.addEventListener("dragover", (ev) => { 2115 | li.style.backgroundColor = "rgba(255, 255, 255, 0.1)"; 2116 | }); 2117 | li.addEventListener("dragleave", (ev) => { 2118 | li.style.backgroundColor = ""; 2119 | }); 2120 | li.addEventListener("drop", (ev) => { 2121 | ev.stopPropagation(); 2122 | let hash = ev.dataTransfer.getData("hash"); 2123 | let pe = ev.target.parentElement; 2124 | let parent = pe.dataset.parent ? `${pe.dataset.parent}/${pe.dataset.name}` : pe.dataset.name; 2125 | fetch(`/api/metadata`, { 2126 | method: "PATCH", 2127 | body: JSON.stringify({ 2128 | hash: hash, 2129 | parent: parent, 2130 | }), 2131 | }).then(() => { 2132 | showSnack("File Moved Successfully", COLOR_GREEN, "success"); 2133 | document.getElementById(`file-${hash}`).remove(); 2134 | }); 2135 | }); 2136 | } else { 2137 | li.draggable = true; 2138 | li.addEventListener("dragstart", (ev) => { 2139 | ev.dataTransfer.setData("hash", file.hash); 2140 | }); 2141 | } 2142 | li.id = `file-${file.hash}`; 2143 | let fileIcon = document.createElement("div"); 2144 | fileIcon.style.color = file.color || "var(--icon-span-color)"; 2145 | let pickerElem = document.createElement("input"); 2146 | pickerElem.type = "color"; 2147 | pickerElem.value = file.color || "var(--icon-span-color)"; 2148 | pickerElem.addEventListener("change", () => { 2149 | file.color = pickerElem.value; 2150 | fetch(`/api/metadata`, { 2151 | method: "PUT", 2152 | body: JSON.stringify(file), 2153 | }).then(() => { 2154 | fileIcon.style.color = file.color; 2155 | showSnack(`Folder color changed successfully`, COLOR_GREEN, "success"); 2156 | }); 2157 | }); 2158 | fileIcon.appendChild(pickerElem); 2159 | fileIcon.className = "file_icon"; 2160 | setIconByMime(file.mime, fileIcon); 2161 | fileIcon.addEventListener("click", (ev) => { 2162 | ev.stopPropagation(); 2163 | if (file.type === "folder") { 2164 | pickerElem.click(); 2165 | return; 2166 | } 2167 | if (!document.querySelector(".multi_select_options")) { 2168 | let multiSelectOptions = document.createElement("div"); 2169 | multiSelectOptions.className = "multi_select_options"; 2170 | 2171 | let menuButton = document.createElement("button"); 2172 | menuButton.innerHTML = 2173 | 'more_horiz'; 2174 | menuButton.addEventListener("click", () => { 2175 | handleMultiSelectMenuClick(); 2176 | }); 2177 | let selectCount = document.createElement("p"); 2178 | selectCount.style.marginRight = "auto"; 2179 | selectCount.id = "selection-count"; 2180 | multiSelectOptions.appendChild(selectCount); 2181 | multiSelectOptions.appendChild(menuButton); 2182 | renderAuxNav(multiSelectOptions); 2183 | } 2184 | if (multiSelectBucketGL.length === 25) { 2185 | showSnack(`Can't select more than 25 items`, COLOR_ORANGE, "warning"); 2186 | return; 2187 | } else { 2188 | fileIcon.innerHTML = `done`; 2189 | let index = multiSelectBucketGL.findIndex((f) => f.hash === file.hash); 2190 | if (index === -1) { 2191 | multiSelectBucketGL.push(file); 2192 | } else { 2193 | multiSelectBucketGL.splice(index, 1); 2194 | setIconByMime(file.mime, fileIcon); 2195 | } 2196 | document.getElementById( 2197 | "selection-count" 2198 | ).innerHTML = `${multiSelectBucketGL.length} selected`; 2199 | if (multiSelectBucketGL.length === 0) { 2200 | navTop.removeChild(navTop.firstChild); 2201 | } 2202 | } 2203 | }); 2204 | let fileInfo = document.createElement("div"); 2205 | fileInfo.className = "info"; 2206 | let fileName = document.createElement("p"); 2207 | fileName.innerHTML = file.name; 2208 | fileName.id = `filename-${file.hash}`; 2209 | let fileSizeAndDate = document.createElement("p"); 2210 | fileSizeAndDate.style.fontSize = "11px"; 2211 | fileSizeAndDate.id = `stat-${file.hash}`; 2212 | if (file.type === "folder") { 2213 | fileSizeAndDate.innerHTML = `${formatDateString(file.date)}`; 2214 | } else { 2215 | fileSizeAndDate.innerHTML = `${handleSizeUnit( 2216 | file.size 2217 | )} • ${formatDateString(file.date)}`; 2218 | } 2219 | fileInfo.appendChild(fileName); 2220 | fileInfo.appendChild(fileSizeAndDate); 2221 | li.appendChild(fileIcon); 2222 | li.appendChild(fileInfo); 2223 | let menuOptionSpan = document.createElement("span"); 2224 | menuOptionSpan.className = "material-symbols-rounded"; 2225 | menuOptionSpan.innerHTML = "more_horiz"; 2226 | menuOptionSpan.style.fontSize = "18px"; 2227 | menuOptionSpan.addEventListener("click", (ev) => { 2228 | ev.stopPropagation(); 2229 | if (trashed) { 2230 | handleTrashFileMenuClick(file); 2231 | } else { 2232 | handleFileMenuClick(file); 2233 | } 2234 | }); 2235 | li.appendChild(menuOptionSpan); 2236 | li.addEventListener("click", (ev) => { 2237 | if (file.type === "folder") { 2238 | handleFolderClick(file); 2239 | } else if (fileName.contentEditable === "true") { 2240 | ev.stopPropagation(); 2241 | return; 2242 | } else { 2243 | showFilePreview(file); 2244 | } 2245 | }); 2246 | li.addEventListener("contextmenu", (ev) => { 2247 | ev.preventDefault(); 2248 | if (fileContextMenuGL) { 2249 | fileContextMenuGL.close(); 2250 | } 2251 | new FileContextMenu(ev, file).show(); 2252 | }); 2253 | return li; 2254 | } 2255 | 2256 | function buildPinnedContent(data) { 2257 | let ul = document.createElement("ul"); 2258 | ul.className = "pinned_files"; 2259 | data.forEach((file) => { 2260 | ul.appendChild(newFileElem(file)); 2261 | }); 2262 | let fileList = document.createElement("div"); 2263 | fileList.className = "file_list"; 2264 | fileList.appendChild(ul); 2265 | return fileList; 2266 | } 2267 | 2268 | function buildRecentContent(data) { 2269 | let ul = document.createElement("ul"); 2270 | ul.className = "recent_files"; 2271 | data.forEach((file) => { 2272 | if (file.parent !== "~shared") { 2273 | ul.appendChild(newFileElem(file)); 2274 | } 2275 | }); 2276 | let fileList = document.createElement("div"); 2277 | fileList.className = "file_list"; 2278 | fileList.appendChild(ul); 2279 | return fileList; 2280 | } 2281 | 2282 | function buildFileBrowser(data) { 2283 | let ul = document.createElement("ul"); 2284 | ul.className = "all_files"; 2285 | data.forEach((file) => { 2286 | ul.appendChild(newFileElem(file)); 2287 | }); 2288 | let fileList = document.createElement("div"); 2289 | fileList.className = "file_list"; 2290 | fileList.appendChild(ul); 2291 | return fileList; 2292 | } 2293 | 2294 | function buildPrompt(folder) { 2295 | let prompt = document.createElement("div"); 2296 | prompt.className = "prompt"; 2297 | let fragment = document.createElement("p"); 2298 | fragment.className = "fragment"; 2299 | let div = document.createElement("div"); 2300 | let backButton = document.createElement("i"); 2301 | backButton.className = "material-symbols-rounded"; 2302 | backButton.innerHTML = "arrow_back"; 2303 | backButton.addEventListener("click", () => { 2304 | if (navTop.firstElementChild.className === "other" && !isFileMovingGL) { 2305 | navTop.firstElementChild.remove(); 2306 | } 2307 | if (!isFileMovingGL) { 2308 | multiSelectBucketGL = []; 2309 | } 2310 | if (!folder.parent) { 2311 | browseButton.click(); 2312 | return; 2313 | } 2314 | let fragments = folder.parent.split("/"); 2315 | handleFolderClick({ 2316 | name: fragments.pop(), 2317 | parent: fragments.length >= 1 ? fragments.join("/") : null, 2318 | }); 2319 | }); 2320 | let selectAll = document.createElement("i"); 2321 | selectAll.className = "material-symbols-rounded"; 2322 | selectAll.innerHTML = "select_all"; 2323 | selectAll.addEventListener("click", () => { 2324 | let targets = []; 2325 | let clickedTargets = []; 2326 | let list = view.children[1].children; 2327 | Array.from(list).forEach((li) => { 2328 | let icon = li.firstElementChild.firstElementChild; 2329 | if (icon.innerHTML === "folder") { 2330 | return; 2331 | } 2332 | icon.innerHTML === "done" 2333 | ? clickedTargets.push(icon) 2334 | : targets.push(icon); 2335 | }); 2336 | targets.slice(0, 25 - clickedTargets.length).forEach((icon) => { 2337 | icon.click(); 2338 | }); 2339 | if (targets.length > 0) { 2340 | selectAll.style.display = "none"; 2341 | deselectAll.style.display = "block"; 2342 | } 2343 | }); 2344 | let deselectAll = document.createElement("i"); 2345 | deselectAll.id = "deselect-all"; 2346 | deselectAll.className = "material-symbols-rounded"; 2347 | deselectAll.innerHTML = "deselect"; 2348 | deselectAll.style.display = "none"; 2349 | deselectAll.addEventListener("click", () => { 2350 | let list = view.children[1].children; 2351 | Array.from(list).forEach((li) => { 2352 | let icon = li.firstElementChild.firstElementChild; 2353 | if (icon.innerHTML === "folder") { 2354 | return; 2355 | } 2356 | icon.innerHTML === "done" ? icon.click() : null; 2357 | }); 2358 | deselectAll.style.display = "none"; 2359 | selectAll.style.display = "block"; 2360 | }); 2361 | prompt.appendChild(backButton); 2362 | div.appendChild(fragment); 2363 | div.appendChild(selectAll); 2364 | div.appendChild(deselectAll); 2365 | prompt.appendChild(div); 2366 | return prompt; 2367 | } 2368 | 2369 | function updateToCompleted(hash) { 2370 | let icon = document.querySelector(`#icon-${hash}`); 2371 | icon.className = "fa-solid fa-check-circle"; 2372 | icon.style.color = "#279627"; 2373 | } 2374 | 2375 | let snackTimer = null; 2376 | function showSnack(text, color = COLOR_GREEN, type = "success") { 2377 | let icons = { 2378 | success: "done", 2379 | error: "cancel", 2380 | warning: "priority_high", 2381 | info: "question_mark", 2382 | }; 2383 | let snackbar = document.querySelector(".snackbar"); 2384 | snackbar.style.display = "flex"; 2385 | let content = document.createElement("div"); 2386 | content.className = "snack_content"; 2387 | content.style.backgroundColor = color; 2388 | let icon = document.createElement("i"); 2389 | icon.className = "material-symbols-rounded"; 2390 | icon.style.marginRight = "10px"; 2391 | icon.innerHTML = icons[type]; 2392 | let p = document.createElement("p"); 2393 | p.innerHTML = text; 2394 | let close = document.createElement("i"); 2395 | close.className = "material-symbols-rounded"; 2396 | close.style.marginLeft = "10px"; 2397 | close.innerHTML = "close"; 2398 | close.style.cursor = "pointer"; 2399 | close.style.backgroundColor = "transparent"; 2400 | close.addEventListener("click", () => { 2401 | snackbar.style.display = "none"; 2402 | }); 2403 | snackbar.innerHTML = ""; 2404 | content.appendChild(icon); 2405 | content.appendChild(p); 2406 | content.appendChild(close); 2407 | snackbar.appendChild(content); 2408 | if (snackTimer) { 2409 | clearTimeout(snackTimer); 2410 | } 2411 | snackTimer = setTimeout(() => { 2412 | snackbar.style.display = "none"; 2413 | }, 3000); 2414 | } 2415 | 2416 | function renderFilesByQuery(query) { 2417 | sidebarOptionSwitch(); 2418 | if (previousOption) { 2419 | previousOption.style.borderLeft = "5px solid transparent"; 2420 | previousOption.style.backgroundColor = "transparent"; 2421 | previousOption = null; 2422 | } 2423 | query["deleted?ne"] = true; 2424 | fetch("/api/query", { method: "POST", body: JSON.stringify(query) }) 2425 | .then((response) => response.json()) 2426 | .then((data) => { 2427 | view.innerHTML = ""; 2428 | if (!data) { 2429 | showSnack("No files found of this type", COLOR_ORANGE, "warning"); 2430 | return; 2431 | } 2432 | let fileList = document.createElement("div"); 2433 | fileList.className = "file_list"; 2434 | let ul = document.createElement("ul"); 2435 | ul.className = "all_files"; 2436 | data.forEach((file) => { 2437 | ul.appendChild(newFileElem(file)); 2438 | }); 2439 | fileList.appendChild(ul); 2440 | view.appendChild(fileList); 2441 | }); 2442 | } 2443 | 2444 | async function loadSharedFile(file, controller, loaderElem) { 2445 | let size = file.size; 2446 | const chunkSize = 1024 * 1024 * 4; 2447 | if (size < chunkSize) { 2448 | let resp = await fetch( 2449 | `/api/external/${userIdGL}/${file.owner}/${file.hash}/0`, 2450 | { signal: controller.signal } 2451 | ); 2452 | loaderElem.innerHTML = "100%"; 2453 | return await resp.blob(); 2454 | } else { 2455 | let skips = 0; 2456 | let progress = 0; 2457 | if (size % chunkSize === 0) { 2458 | skips = size / chunkSize; 2459 | } else { 2460 | skips = Math.floor(size / chunkSize) + 1; 2461 | } 2462 | let heads = Array.from(Array(skips).keys()); 2463 | let promises = []; 2464 | heads.forEach((head) => { 2465 | promises.push( 2466 | fetch(`/api/external/${userIdGL}/${file.owner}/${file.hash}/${head}`) 2467 | .then((resp) => { 2468 | return resp.blob(); 2469 | }) 2470 | .then((blob) => { 2471 | progress += blob.size; 2472 | let percentage = Math.floor((progress / size) * 100); 2473 | loaderElem.innerHTML = `${percentage}%`; 2474 | return blob; 2475 | }) 2476 | ); 2477 | }); 2478 | let blobs = await Promise.all(promises); 2479 | return new Blob(blobs, { type: file.mime }); 2480 | } 2481 | } 2482 | 2483 | // this will suck at large files 2484 | async function showFilePreview(file) { 2485 | previewModal.innerHTML = ""; 2486 | previewModal.style.display = "flex"; 2487 | previewModal.style.outline = "none"; 2488 | let description = document.createElement("p"); 2489 | description.innerHTML = ` 2490 | ${file.name} 2491 | 0% 2497 | 2498 | `; 2499 | previewModal.appendChild(description); 2500 | let openInNew = document.createElement("span"); 2501 | openInNew.title = "Open in new tab"; 2502 | openInNew.className = "material-symbols-rounded"; 2503 | openInNew.innerHTML = "open_in_new"; 2504 | openInNew.style.opacity = 0.5; 2505 | openInNew.addEventListener("click", () => { 2506 | window.open(blobURL, "_blank"); 2507 | }); 2508 | let stop = document.createElement("span"); 2509 | stop.title = "Stop loading"; 2510 | stop.className = "material-symbols-rounded"; 2511 | stop.innerHTML = "close"; 2512 | stop.style.color = COLOR_RED; 2513 | stop.style.pointerEvents = "all"; 2514 | stop.addEventListener("click", () => { 2515 | controller.abort(); 2516 | previewModal.innerHTML = ""; 2517 | previewModal.style.display = "none"; 2518 | previewModal.close(); 2519 | }); 2520 | let download = document.createElement("span"); 2521 | download.title = "Download"; 2522 | download.className = "material-symbols-rounded"; 2523 | download.innerHTML = "download"; 2524 | download.style.opacity = 0.5; 2525 | download.addEventListener("click", () => { 2526 | let a = document.createElement("a"); 2527 | a.href = blobURL; 2528 | a.download = file.name; 2529 | a.click(); 2530 | }); 2531 | previewModal.appendChild(stop); 2532 | previewModal.appendChild(download); 2533 | previewModal.appendChild(openInNew); 2534 | previewModal.showModal(); 2535 | let loaderAmount = document.querySelector("#loader-amount"); 2536 | controller = new AbortController(); 2537 | let blobURL; 2538 | if (file.shared) { 2539 | let blob = await loadSharedFile(file, controller, loaderAmount); 2540 | blobURL = URL.createObjectURL(new Blob([blob], { type: file.mime })); 2541 | loaderAmount.innerHTML = "100%"; 2542 | openInNew.style.opacity = 1; 2543 | download.style.opacity = 1; 2544 | openInNew.style.pointerEvents = "all"; 2545 | download.style.pointerEvents = "all"; 2546 | } else { 2547 | let extRegex = /(?:\.([^.]+))?$/; 2548 | let extension = extRegex.exec(file.name); 2549 | if (extension && extension[1]) { 2550 | extension = extension[1]; 2551 | } else { 2552 | extension = ""; 2553 | } 2554 | let filename; 2555 | if (extension === "") { 2556 | filename = file.hash; 2557 | } else { 2558 | filename = `${file.hash}.${extension}`; 2559 | } 2560 | let projectId = secretKeyGL.split("_")[0]; 2561 | let url = `https://drive.deta.sh/v1/${projectId}/filebox/files/download?name=${filename}`; 2562 | const response = await fetch(url, { 2563 | headers: { "X-Api-Key": secretKeyGL }, 2564 | signal: controller.signal, 2565 | }); 2566 | if (response.status !== 200) { 2567 | loaderAmount.style.color = COLOR_RED; 2568 | loaderAmount.innerHTML = `Error ${response.status}` 2569 | } else { 2570 | let progress = 0; 2571 | const reader = response.body.getReader(); 2572 | const stream = new ReadableStream({ 2573 | start(controller) { 2574 | return pump(); 2575 | function pump() { 2576 | return reader.read().then(({ done, value }) => { 2577 | if (done) { 2578 | controller.close(); 2579 | return; 2580 | } 2581 | controller.enqueue(value); 2582 | progress += value.length; 2583 | let percentage = Math.floor((progress / file.size) * 100); 2584 | loaderAmount.innerHTML = `${percentage}%`; 2585 | return pump(); 2586 | }); 2587 | } 2588 | }, 2589 | }); 2590 | const rs = new Response(stream); 2591 | const blob = await rs.blob(); 2592 | blobURL = URL.createObjectURL(new Blob([blob], { type: file.mime })); 2593 | openInNew.style.pointerEvents = "all"; 2594 | download.style.pointerEvents = "all"; 2595 | openInNew.style.opacity = 1; 2596 | download.style.opacity = 1; 2597 | } 2598 | } 2599 | } 2600 | 2601 | function fileMover(file) { 2602 | let fileMover = document.createElement("div"); 2603 | fileMover.className = "file_mover"; 2604 | let cancelButton = document.createElement("button"); 2605 | cancelButton.innerHTML = "Cancel"; 2606 | cancelButton.addEventListener("click", () => { 2607 | navTop.removeChild(navTop.firstChild); 2608 | }); 2609 | let selectButton = document.createElement("button"); 2610 | selectButton.innerHTML = "Select"; 2611 | selectButton.style.backgroundColor = "var(--accent-blue)"; 2612 | selectButton.addEventListener("click", () => { 2613 | if (!openedFolderGL) { 2614 | file.parent = null; 2615 | } else { 2616 | if (openedFolderGL.parent) { 2617 | file.parent = `${openedFolderGL.parent}/${openedFolderGL.name}`; 2618 | } else { 2619 | file.parent = openedFolderGL.name; 2620 | } 2621 | } 2622 | fetch(`/api/metadata`, { 2623 | method: "PATCH", 2624 | body: JSON.stringify({ 2625 | hash: file.hash, 2626 | parent: file.parent, 2627 | }), 2628 | }).then(() => { 2629 | if (document.querySelector(`#file-${file.hash}`)) { 2630 | showSnack("File is already here", COLOR_ORANGE, "info"); 2631 | return; 2632 | } 2633 | showSnack("File Moved Successfully", COLOR_GREEN, "success"); 2634 | navTop.removeChild(navTop.firstChild); 2635 | if (openedFolderGL) { 2636 | handleFolderClick(openedFolderGL); 2637 | } else { 2638 | browseButton.click(); 2639 | } 2640 | }); 2641 | }); 2642 | let p = document.createElement("p"); 2643 | p.innerHTML = "Select Move Destination"; 2644 | fileMover.appendChild(cancelButton); 2645 | fileMover.appendChild(p); 2646 | fileMover.appendChild(selectButton); 2647 | return fileMover; 2648 | } 2649 | 2650 | function renderSearchResults(query) { 2651 | fetch(`/api/query`, { 2652 | method: "POST", 2653 | body: JSON.stringify(query), 2654 | }) 2655 | .then((response) => response.json()) 2656 | .then((data) => { 2657 | let key = Object.keys(query)[0]; 2658 | let attr = key.replace("?contains", ""); 2659 | let value = query[key]; 2660 | view.innerHTML = ""; 2661 | if (!data) { 2662 | let p = document.createElement("p"); 2663 | let symbol = ` `; 2664 | p.innerHTML = `${symbol} No results found for ${attr}: *${value}*`; 2665 | p.style.backgroundColor = "#e44d27"; 2666 | view.appendChild(p); 2667 | return; 2668 | } 2669 | let absoluteResults = data.filter((file) => { 2670 | if (file.name.startsWith(query)) { 2671 | data.splice(data.indexOf(file), 1); 2672 | return true; 2673 | } else { 2674 | return false; 2675 | } 2676 | }); 2677 | data = absoluteResults.concat(data); 2678 | let p = document.createElement("p"); 2679 | p.innerHTML = `Search results for ${attr}: *${value}*`; 2680 | p.style.backgroundColor = "#317840"; 2681 | view.appendChild(p); 2682 | let list = document.createElement("ul"); 2683 | view.appendChild(list); 2684 | data.forEach((file) => { 2685 | list.appendChild(newFileElem(file)); 2686 | }); 2687 | }); 2688 | } 2689 | 2690 | function renderAuxNav(elem) { 2691 | let wrapper = document.createElement("div"); 2692 | wrapper.className = "other"; 2693 | wrapper.appendChild(elem); 2694 | navTop.prepend(wrapper); 2695 | } 2696 | 2697 | function renderFileSenderModal(file) { 2698 | if (!userIdGL) { 2699 | showSnack( 2700 | "File sending is not available for this instance", 2701 | COLOR_ORANGE, 2702 | "info" 2703 | ); 2704 | return; 2705 | } 2706 | let fileSender = document.createElement("div"); 2707 | fileSender.className = "file_sender"; 2708 | let filename = document.createElement("p"); 2709 | filename.innerHTML = file.name; 2710 | let userIdField = document.createElement("input"); 2711 | userIdField.placeholder = "Type user instance id"; 2712 | userIdField.type = "text"; 2713 | userIdField.spellcheck = false; 2714 | let buttons = document.createElement("div"); 2715 | let cancelButton = document.createElement("button"); 2716 | cancelButton.innerHTML = "Cancel"; 2717 | cancelButton.addEventListener("click", () => { 2718 | hideRightNav(); 2719 | }); 2720 | let sendButton = document.createElement("button"); 2721 | sendButton.innerHTML = "Send"; 2722 | sendButton.style.backgroundColor = COLOR_GREEN; 2723 | sendButton.addEventListener("click", () => { 2724 | if (userIdField.value === userIdGL) { 2725 | showSnack("You can't send a file to yourself", COLOR_ORANGE, "warning"); 2726 | return; 2727 | } 2728 | let fileClone = structuredClone(file); 2729 | delete fileClone.recipients; 2730 | delete fileClone.pinned; 2731 | fileClone.owner = userIdGL; 2732 | fileClone.shared = true; 2733 | fileClone.parent = "~shared"; 2734 | fetch(`/api/push/${userIdField.value}`, { 2735 | method: "POST", 2736 | body: JSON.stringify(fileClone), 2737 | }).then((resp) => { 2738 | if (resp.status !== 207) { 2739 | fileSender.style.display = "none"; 2740 | showSnack("Something went wrong. Please try again", COLOR_RED, "error"); 2741 | return; 2742 | } 2743 | if (file.recipients) { 2744 | if (!file.recipients.includes(userIdField.value)) { 2745 | file.recipients.push(userIdField.value); 2746 | } 2747 | } else { 2748 | file.recipients = [userIdField.value]; 2749 | } 2750 | fetch(`/api/metadata`, { 2751 | method: "PUT", 2752 | body: JSON.stringify(file), 2753 | }).then((resp) => { 2754 | if (resp.status === 207) { 2755 | showSnack( 2756 | `File shared with ${userIdField.value}`, 2757 | COLOR_GREEN, 2758 | "success" 2759 | ); 2760 | fileSender.style.display = "none"; 2761 | } else { 2762 | showSnack( 2763 | "Something went wrong. Please try again", 2764 | COLOR_RED, 2765 | "error" 2766 | ); 2767 | } 2768 | }); 2769 | }); 2770 | }); 2771 | buttons.appendChild(cancelButton); 2772 | buttons.appendChild(sendButton); 2773 | fileSender.appendChild(filename); 2774 | fileSender.appendChild(userIdField); 2775 | fileSender.appendChild(buttons); 2776 | renderInRightNav(fileSender); 2777 | } 2778 | 2779 | function renderGreetings() { 2780 | let greetings = document.createElement("div"); 2781 | greetings.className = "greetings"; 2782 | let skip = document.createElement("div"); 2783 | skip.className = "skip"; 2784 | skip.innerHTML = "skip
"; 2785 | skip.addEventListener("click", () => { 2786 | localStorage.setItem("isGreeted", true); 2787 | greetings.remove(); 2788 | }); 2789 | let innerOne = document.createElement("div"); 2790 | innerOne.className = "inner"; 2791 | innerOne.innerHTML = '${text}
` 42 | let div = document.createElement("div"); 43 | let backButton = document.createElement("i"); 44 | backButton.className = "material-symbols-rounded"; 45 | backButton.innerHTML = "arrow_back"; 46 | backButton.style.color = "white"; 47 | backButton.addEventListener("click", async () => { 48 | if (folder.parent === root) { 49 | return; 50 | } 51 | if (folder.parent) { 52 | let parent = folder.parent.split("/"); 53 | let name = parent.pop(); 54 | parent = parent.join("/"); 55 | await renderFolder({parent: parent, name: name}); 56 | } 57 | }); 58 | prompt.appendChild(fragment); 59 | div.appendChild(backButton); 60 | prompt.appendChild(div); 61 | return prompt; 62 | } 63 | 64 | 65 | function fileElem(file) { 66 | let li = document.createElement("li"); 67 | li.id = `file-${file.hash}`; 68 | li.style.padding = "0"; 69 | let fileIcon = document.createElement("div"); 70 | fileIcon.classList.add("file_icon"); 71 | setIconByMime(file.mime, fileIcon); 72 | fileIcon.style.color = file.color || "var(--icon-span-color)"; 73 | li.appendChild(fileIcon); 74 | let info = document.createElement("div"); 75 | info.classList.add("info"); 76 | let fileName = document.createElement("p"); 77 | fileName.innerText = file.name; 78 | let fileSizeAndDate = document.createElement("p"); 79 | fileSizeAndDate.style.fontSize = "12px"; 80 | if (file.type !== "folder") { 81 | fileSizeAndDate.innerHTML = `${handleSizeUnit(file.size)} • ${formatDateString(file.date)}`; 82 | } else { 83 | fileSizeAndDate.innerHTML = `${formatDateString(file.date)}`; 84 | } 85 | info.appendChild(fileName); 86 | info.appendChild(fileSizeAndDate); 87 | li.appendChild(info); 88 | let progressEL = document.createElement("span"); 89 | progressEL.style.color = COLOR_GREEN; 90 | progressEL.style.display = "none"; 91 | progressEL.style.fontSize = "18px"; 92 | progressEL.classList.add("material-symbols-rounded"); 93 | progressEL.innerText = "progress_activity"; 94 | progressEL.addEventListener("click", async () => { 95 | showRightNav(); 96 | }); 97 | li.appendChild(progressEL); 98 | let download = document.createElement("span"); 99 | progressEL.style.marginLeft = "10px"; 100 | download.style.fontSize = "22px"; 101 | download.classList.add("material-symbols-rounded"); 102 | download.innerText = "download"; 103 | download.addEventListener("click", async () => { 104 | blurLayer.style.display = "block"; 105 | navRight.style.display = "flex"; 106 | progressEL.style.display = "block"; 107 | download.style.display = "none"; 108 | let angle = 0; 109 | let interval = setInterval(() => { 110 | angle += 3; 111 | progressEL.style.transform = `rotate(${angle}deg)`; 112 | }, 10); 113 | const chunkSize = 1024 * 1024 * 4; 114 | let taskEL = document.querySelector(`#task-${file.hash}`); 115 | if (taskEL) { 116 | taskEL.remove(); 117 | } 118 | prependQueueElem(file); 119 | let bar = document.querySelector(`#bar-${file.hash}`); 120 | let percentage = document.querySelector(`#percentage-${file.hash}`); 121 | if (file.size < chunkSize) { 122 | const resp = await fetch(`/api/download/na/${file.hash}/0`); 123 | if (resp.status === 403) { 124 | alert(`File access denied by owner!`); 125 | window.location.reload(); 126 | } else { 127 | let a = document.createElement("a"); 128 | a.href = URL.createObjectURL(await resp.blob()); 129 | bar.style.width = "100%"; 130 | percentage.innerHTML = "100%"; 131 | a.download = file.name; 132 | a.click(); 133 | } 134 | } else { 135 | let skips = 0; 136 | if (file.size % chunkSize === 0) { 137 | skips = file.size / chunkSize; 138 | } else { 139 | skips = Math.floor(file.size / chunkSize) + 1; 140 | } 141 | let heads = Array.from(Array(skips).keys()); 142 | let promises = []; 143 | let progress = 0; 144 | heads.forEach((head) => { 145 | promises.push( 146 | fetch(`/api/download/na/${file.hash}/${head}`) 147 | .then((response) => { 148 | if (response.status === 403) { 149 | alert(`File access denied by owner!`); 150 | window.location.reload(); 151 | } else if (response.status === 502) { 152 | alert(`Server refused to deliver chunk ${head}, try again!`); 153 | window.location.reload(); 154 | } else { 155 | return response.blob(); 156 | } 157 | }) 158 | .then((blob) => { 159 | progress++; 160 | bar.style.width = `${Math.round((progress / skips) * 100)}%`; 161 | percentage.innerHTML = `${Math.round((progress / skips) * 100)}%`; 162 | return blob; 163 | }) 164 | ); 165 | }); 166 | const blobs = await Promise.all(promises); 167 | let blob = new Blob(blobs, { type: file.mime }); 168 | let a = document.createElement("a"); 169 | a.href = URL.createObjectURL(blob);; 170 | bar.style.width = "100%"; 171 | percentage.innerHTML = "100%"; 172 | a.download = file.name; 173 | a.click(); 174 | } 175 | clearInterval(interval); 176 | progressEL.style.display = "none"; 177 | download.style.display = "block"; 178 | }); 179 | if (file.type !== "folder") { 180 | li.appendChild(download); 181 | } 182 | if (file.type === "folder") { 183 | li.addEventListener("click", async () => { 184 | await renderFolder(file); 185 | }); 186 | } 187 | return li; 188 | } 189 | 190 | async function renderFolder(file) { 191 | let parent = file.parent ? `${file.parent}/${file.name}` : file.name; 192 | const resp = await fetch(`/api/query`, { 193 | method: "POST", 194 | body: JSON.stringify({ 195 | "__union": true, 196 | "__queries": [ 197 | { 198 | "parent": parent, 199 | "type": "folder", 200 | }, 201 | { 202 | "parent": parent, 203 | "access": "public", 204 | } 205 | ] 206 | }), 207 | }) 208 | let ul = document.createElement("ul"); 209 | let children = await resp.json(); 210 | children = children.sort((a, b) => { 211 | if (a.type === "folder" && b.type !== "folder") { 212 | return -1; 213 | } else if (a.type !== "folder" && b.type === "folder") { 214 | return 1; 215 | } else { 216 | return 0; 217 | } 218 | }); 219 | children.forEach((child) => { 220 | ul.appendChild(fileElem(child)); 221 | }); 222 | view.innerHTML = ""; 223 | view.appendChild(promptElem(file)); 224 | view.appendChild(ul); 225 | }; 226 | 227 | 228 | function prependQueueElem(file) { 229 | let li = document.createElement("li"); 230 | li.id = `task-${file.hash}`; 231 | let icon = document.createElement("div"); 232 | icon.className = "icon"; 233 | setIconByMime(file.mime, icon); 234 | let info = document.createElement("div"); 235 | info.className = "info"; 236 | let name = document.createElement("p"); 237 | name.innerHTML = file.name; 238 | let progress = document.createElement("div"); 239 | progress.className = "progress"; 240 | let bar = document.createElement("div"); 241 | bar.className = "bar"; 242 | bar.style.width = "0%"; 243 | bar.id = `bar-${file.hash}`; 244 | bar.style.backgroundColor = COLOR_GREEN; 245 | progress.appendChild(bar); 246 | info.appendChild(name); 247 | info.appendChild(progress); 248 | let percentage = document.createElement("p"); 249 | percentage.innerHTML = "0%"; 250 | percentage.id = `percentage-${file.hash}`; 251 | li.appendChild(icon); 252 | li.appendChild(info); 253 | li.appendChild(percentage); 254 | tasksElem.appendChild(li); 255 | showRightNav(); 256 | } 257 | 258 | 259 | function showRightNav() { 260 | if (navRight.style.display === "none") { 261 | blurLayer.style.display = "block"; 262 | navRight.style.display = "flex"; 263 | } 264 | } 265 | 266 | function closeRightNav() { 267 | blurLayer.style.display = "none"; 268 | navRight.style.display = "none"; 269 | } 270 | 271 | function handleSizeUnit(size) { 272 | if (size === undefined) { 273 | return "~"; 274 | } 275 | if (size < 1024) { 276 | return size + " B"; 277 | } else if (size < 1024 * 1024) { 278 | return (size / 1024).toFixed(2) + " KB"; 279 | } else if (size < 1024 * 1024 * 1024) { 280 | return (size / 1024 / 1024).toFixed(2) + " MB"; 281 | } else { 282 | return (size / 1024 / 1024 / 1024).toFixed(2) + " GB"; 283 | } 284 | } 285 | 286 | function formatDateString(date) { 287 | date = new Date(date); 288 | return ` 289 | ${date.toLocaleString("default", { month: "short" })} 290 | ${date.getDate()}, 291 | ${date.getFullYear()} 292 | ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()} 293 | `; 294 | } 295 | 296 | function setIconByMime(mime, elem) { 297 | if (mime === undefined) { 298 | elem.innerHTML = `folder`; 299 | } else if (mime.startsWith("image")) { 300 | elem.innerHTML = `image`; 301 | } else if (mime.startsWith("video")) { 302 | elem.innerHTML = `movie`; 303 | } else if (mime.startsWith("audio")) { 304 | elem.innerHTML = `music_note`; 305 | } else if (mime.startsWith("text")) { 306 | elem.innerHTML = `text_snippet`; 307 | } else if (mime.startsWith("application/pdf")) { 308 | elem.innerHTML = `book`; 309 | } else if (mime.startsWith("application/zip")) { 310 | elem.innerHTML = `archive`; 311 | } else if (mime.startsWith("application/x-rar-compressed")) { 312 | elem.innerHTML = `archive`; 313 | } else if (mime.startsWith("font")) { 314 | elem.innerHTML = `format_size`; 315 | } else { 316 | elem.innerHTML = `draft`; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /static/styles/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: 'Poppins', sans-serif; 6 | } 7 | 8 | :root { 9 | --accent-blue: #0561da; 10 | --bg-primary: #1a1c20; 11 | --bg-secondary: #ffffff0c; 12 | --bg-secondary-solid: #2d2f32; 13 | --border-primary: #ffffff0c; 14 | --border-secondary: #cccccc09; 15 | --blur-bg: rgba(110,118,129,0.4); 16 | --scrollbar-track-bg: #1919194d; 17 | --shadow: rgba(0, 0, 0, 0.425); 18 | --text-color: #ccc; 19 | --icon-span-color: #ccc; 20 | --filemenu-span-bg: #ffffff0c; 21 | --user-span-color: #d2e4ffbf; 22 | --nav-left-option-bg: #0561da31; 23 | --progress-bar-bg: #2d2f32; 24 | --white-text: #ffffff; 25 | } 26 | 27 | .light-mode { 28 | --bg-primary: whitesmoke; 29 | --bg-secondary: #0000000c; 30 | --bg-secondary-solid: #2d2f32; 31 | --border-primary: #0000000c; 32 | --border-secondary: #02020209; 33 | --blur-bg: rgba(110,118,129,0.4); 34 | --scrollbar-track-bg: #1919194d; 35 | --shadow: rgba(0, 0, 0, 0.425); 36 | --text-color: #3b3a3ace; 37 | --icon-span-color: #4b4545; 38 | --filemenu-span-bg: #08080813; 39 | --user-span-color: #747477; 40 | --nav-left-option-bg: #2e323810; 41 | --progress-bar-bg: #2d2f3242; 42 | } 43 | 44 | ::-webkit-scrollbar { 45 | width: 5px; 46 | height: 8px; 47 | } 48 | 49 | ::-webkit-scrollbar-track { 50 | background: var(--scrollbar-track-bg); 51 | } 52 | 53 | 54 | ::-webkit-scrollbar-thumb { 55 | background: var(--bg-secondary-solid); 56 | } 57 | 58 | body { 59 | height: 100vh; 60 | height: 100dvh; 61 | overflow: hidden; 62 | background-color: var(--bg-primary); 63 | } 64 | 65 | .container { 66 | width: 100%; 67 | height: 100%; 68 | display: flex; 69 | align-items: center; 70 | justify-content: center; 71 | } 72 | 73 | .blur_layer { 74 | display: none; 75 | width: 100%; 76 | height: 100%; 77 | position: absolute; 78 | z-index: 99; 79 | background-color: var(--blur-bg); 80 | } 81 | 82 | @keyframes sildein_ltr { 83 | 0% { 84 | left: -150px; 85 | } 86 | 100% { 87 | left: 0; 88 | } 89 | } 90 | 91 | .nav_right { 92 | right: 0; 93 | position: fixed; 94 | width: 500px; 95 | height: 100vh; 96 | height: 100dvh; 97 | display: none; 98 | flex-direction: column; 99 | justify-content: flex-start; 100 | align-items: center; 101 | z-index: 10000 !important; 102 | animation: sildein_rtl 0.5s; 103 | background-color: var(--bg-primary); 104 | box-shadow: 0 0 10px 0 var(--shadow); 105 | } 106 | 107 | @media screen and (max-width: 768px) { 108 | .nav_right { 109 | width: 300px; 110 | } 111 | } 112 | 113 | .nav_left { 114 | width: 250px; 115 | height: 100%; 116 | height: 100dvh; 117 | background-color: var(--bg-primary); 118 | color: var(--text-color); 119 | display: flex; 120 | flex-direction: column; 121 | justify-content: flex-start; 122 | align-items: center; 123 | z-index: 100; 124 | overflow-y: auto; 125 | border-right: 1px solid var(--border-primary); 126 | animation: sildein_ltr 0.5s; 127 | padding: 20px; 128 | padding-bottom: 10px; 129 | } 130 | .nav_left > .logo { 131 | width: 100%; 132 | height: 150px; 133 | display: flex; 134 | flex-direction: row; 135 | justify-content: center; 136 | align-items: center; 137 | flex-shrink: 0; 138 | background-color: var(--bg-secondary); 139 | border: 1px solid var(--border-secondary); 140 | border-radius: 5px; 141 | margin-bottom: 20px; 142 | cursor: pointer; 143 | } 144 | .nav_left > .logo > img { 145 | width: 40px; 146 | height: 40px; 147 | margin-right: 10px; 148 | } 149 | .nav_left > .logo > p { 150 | font-size: 20px; 151 | color: var(--text-color); 152 | } 153 | .nav_left > .wrapper { 154 | width: 100%; 155 | height: 100%; 156 | display: flex; 157 | flex-direction: column; 158 | align-items: center; 159 | justify-content: flex-start; 160 | } 161 | .nav_left_option { 162 | width: 100%; 163 | height: 42px; 164 | font-size: 16px; 165 | cursor: pointer; 166 | display: flex; 167 | align-items: center; 168 | justify-content: flex-start; 169 | background-color: transparent; 170 | border-radius: 5px; 171 | margin: 5px 0; 172 | } 173 | .nav_left_option > .icon { 174 | width: 40px; 175 | height: 40px; 176 | display: flex; 177 | align-items: center; 178 | justify-content: center; 179 | } 180 | .nav_left_option > .icon > span { 181 | font-size: 20px; 182 | color: var(--icon-span-color); 183 | z-index: 2; 184 | } 185 | .nav_left_option:hover { 186 | background-color: #ffffff1c; 187 | } 188 | 189 | .user { 190 | width: 100%; 191 | height: max-content; 192 | display: flex; 193 | padding: 5px; 194 | align-items: center; 195 | justify-content: space-between; 196 | z-index: 10; 197 | border-radius: 30px; 198 | } 199 | .user > p { 200 | max-width: 125px; 201 | font-size: 15px; 202 | color: var(--text-color); 203 | padding: 5px 14px; 204 | border-radius: 16px; 205 | text-overflow: ellipsis; 206 | overflow: hidden; 207 | white-space: nowrap; 208 | text-align: end; 209 | background-color: var(--border-primary); 210 | cursor: copy; 211 | } 212 | .user > p:hover { 213 | background-color: var(--bg-primary); 214 | } 215 | .user > span { 216 | font-size: 16px; 217 | color: var(--user-span-color); 218 | cursor: pointer; 219 | padding: 8px; 220 | border-radius: 50%; 221 | background-color: var(--border-primary); 222 | } 223 | 224 | .content { 225 | width: calc(100% - 250px); 226 | height: 100%; 227 | background-color: transparent; 228 | color: var(--text-color); 229 | display: flex; 230 | flex-direction: column; 231 | justify-content: flex-start; 232 | align-items: center; 233 | z-index: 1; 234 | overflow: hidden; 235 | } 236 | .content > nav { 237 | width: 100%; 238 | height: 60px; 239 | display: flex; 240 | flex-shrink: 0; 241 | align-items: center; 242 | padding: 0 15px; 243 | justify-content: flex-start; 244 | z-index: 2; 245 | padding: 0 10px; 246 | } 247 | .content > nav > span { 248 | cursor: pointer; 249 | font-size: 20px; 250 | font-weight: 500; 251 | margin: 10px; 252 | padding: 5px; 253 | border-radius: 5px; 254 | background-color: var(--bg-secondary); 255 | border: 1px solid var(--border-secondary); 256 | } 257 | .content > nav > input[type="text"] { 258 | width: 100%; 259 | max-width: 500px; 260 | background-color: transparent; 261 | border: none; 262 | outline: none; 263 | color: var(--text-color); 264 | font-size: 16px; 265 | padding: 7px 15px; 266 | border-radius: 20px; 267 | background-color: var(--bg-secondary); 268 | border: 1px solid var(--border-secondary); 269 | transition: all 0.2s ease-in-out; 270 | padding-right: 50px; 271 | margin: 0 10px; 272 | } 273 | .content > nav > div > button { 274 | height: max-content; 275 | background-color: var(--bg-secondary); 276 | border: none; 277 | outline: none; 278 | color: #ccc; 279 | padding: 8px; 280 | cursor: pointer; 281 | white-space: nowrap; 282 | margin-left: 10px; 283 | display: flex; 284 | align-items: center; 285 | justify-content: center; 286 | border-radius: 50%; 287 | } 288 | .content > nav > div > button > span { 289 | font-size: 20px; 290 | color: var(--user-span-color); 291 | } 292 | .content > nav > .other { 293 | width: 100%; 294 | height: 100%; 295 | display: flex; 296 | padding: 5px; 297 | align-items: center; 298 | justify-content: center; 299 | flex-shrink: 0; 300 | margin-right: 5px; 301 | } 302 | 303 | .content > nav > .other > .multi_select_options { 304 | width: 100%; 305 | display: flex; 306 | align-items: center; 307 | justify-content: space-between; 308 | } 309 | 310 | .content > nav > .other > .multi_select_options > button { 311 | display: flex; 312 | align-items: center; 313 | justify-content: center; 314 | background-color: var(--bg-secondary); 315 | border: none; 316 | outline: none; 317 | color: var(--icon-span-color); 318 | padding: 5px; 319 | border-radius: 50%; 320 | cursor: pointer; 321 | } 322 | 323 | #menu { 324 | display: none; 325 | } 326 | 327 | #search-icon { 328 | margin-left: -60px; 329 | border-left: 1px solid var(--border-primary); 330 | } 331 | 332 | @media screen and (max-width: 768px) { 333 | .nav_left { 334 | top: 0; 335 | left: 0; 336 | display: none; 337 | position: fixed; 338 | } 339 | .content { 340 | width: 100%; 341 | } 342 | 343 | .content > nav > input[type="text"] { 344 | font-size: 14px; 345 | } 346 | .content > nav > div > button { 347 | font-size: 12px; 348 | padding: 6px; 349 | } 350 | #menu { 351 | display: flex; 352 | } 353 | #search-icon { 354 | display: none; 355 | } 356 | } 357 | 358 | main { 359 | width: 100%; 360 | height: 100%; 361 | padding: 0 10px; 362 | display: flex; 363 | flex-direction: column; 364 | justify-content: flex-start; 365 | align-items: center; 366 | overflow-y: auto; 367 | overflow-x: hidden; 368 | } 369 | main > p { 370 | font-size: 12px; 371 | color: var(--white-text); 372 | margin-top: 10px; 373 | padding: 4px 15px; 374 | border-radius: 15px; 375 | background-color: orangered; 376 | } 377 | main > ul { 378 | width: 100%; 379 | height: 100%; 380 | display: flex; 381 | flex-direction: column; 382 | justify-content: flex-start; 383 | align-items: flex-start; 384 | overflow-y: auto; 385 | } 386 | main > ul > li { 387 | width: 100%; 388 | height: 60px; 389 | display: flex; 390 | align-items: center; 391 | cursor: pointer; 392 | justify-content: space-between; 393 | } 394 | main > ul > li > .file_icon { 395 | padding: 10px; 396 | border-radius: 5px; 397 | display: flex; 398 | justify-content: center; 399 | align-items: center; 400 | font-size: 25px; 401 | color: var(--icon-span-color); 402 | background-color: var(--bg-secondary); 403 | border: 1px solid var(--border-secondary); 404 | margin: 0 10px; 405 | } 406 | main > ul > li:hover > .file_icon { 407 | border: 1px dashed #cccccc7e; 408 | } 409 | main > ul > li > span { 410 | color: var(--icon-span-color); 411 | padding: 5px; 412 | margin: 10px; 413 | border-radius: 50%; 414 | cursor: pointer; 415 | background-color: var(--filemenu-span-bg); 416 | } 417 | main > ul > li > .info { 418 | width: 100%; 419 | height: 60px; 420 | display: flex; 421 | flex-direction: column; 422 | justify-content: center; 423 | align-items: flex-start; 424 | padding-left: 5px; 425 | color: var(--text-color); 426 | } 427 | main > ul > li > .info > p { 428 | max-width: 650px; 429 | font-size: 15px; 430 | overflow: hidden; 431 | white-space: nowrap; 432 | text-align: left; 433 | text-overflow: ellipsis; 434 | } 435 | @media screen and (max-width: 768px) { 436 | main > ul > li { 437 | height: 50px; 438 | } 439 | main > ul > li > span { 440 | font-size: 10px; 441 | } 442 | main > ul > li > .info { 443 | height: 50px; 444 | } 445 | main > ul > li > .info > p { 446 | width: 230px; 447 | font-size: 12px; 448 | } 449 | } 450 | 451 | @keyframes sildein_rtl { 452 | 0% { 453 | right: -150px; 454 | } 455 | 100% { 456 | right: 0; 457 | } 458 | } 459 | 460 | .file_menu { 461 | width: 100%; 462 | height: 100%; 463 | display: flex; 464 | flex-direction: column; 465 | justify-content: flex-start; 466 | align-items: center; 467 | } 468 | .file_menu > .title { 469 | width: 100%; 470 | height: 60px; 471 | display: flex; 472 | align-items: center; 473 | padding: 0 20px; 474 | justify-content: space-between; 475 | background-color: #ffffff0c; 476 | } 477 | .file_menu > .title > p { 478 | width: 400px; 479 | font-size: 18px; 480 | color: whitesmoke; 481 | overflow: hidden; 482 | white-space: nowrap; 483 | text-overflow: ellipsis; 484 | } 485 | .file_menu > .title > span { 486 | font-size: 25px; 487 | cursor: pointer; 488 | padding: 5px; 489 | color: var(--icon-span-color); 490 | } 491 | .file_menu > .file_menu_option { 492 | width: 100%; 493 | height: 50px; 494 | display: flex; 495 | align-items: center; 496 | padding-left: 20px; 497 | padding-right: 20px; 498 | justify-content: space-between; 499 | border-bottom: 1px solid var(--border-primary); 500 | cursor: pointer; 501 | } 502 | .file_menu > .file_menu_option:hover { 503 | background-color: var(--bg-secondary); 504 | } 505 | .file_menu > .file_menu_option > p { 506 | font-size: 15px; 507 | color: var(--text-color); 508 | max-width: 100%; 509 | white-space: nowrap; 510 | overflow: hidden; 511 | text-overflow: ellipsis; 512 | } 513 | .file_menu > .file_menu_option > span { 514 | font-size: 15px; 515 | color: var(--icon-span-color); 516 | padding: 10px; 517 | border-radius: 50%; 518 | cursor: pointer; 519 | background-color: var(--bg-secondary); 520 | } 521 | 522 | @media screen and (max-width: 768px) { 523 | .file_menu { 524 | width: 300px; 525 | } 526 | .file_menu > .title { 527 | height: 50px; 528 | padding: 0 10px; 529 | } 530 | .file_menu > .title > p { 531 | width: 300px; 532 | font-size: 15px; 533 | } 534 | .file_menu > .file_menu_option { 535 | height: 40px; 536 | padding: 0 12px; 537 | } 538 | .file_menu > .file_menu_option > p { 539 | font-size: 14px; 540 | } 541 | .file_menu > .file_menu_option > span { 542 | font-size: 12px; 543 | padding: 8px; 544 | } 545 | } 546 | 547 | .prompt { 548 | width: 100%; 549 | height: 50px; 550 | display: flex; 551 | flex-shrink: 0; 552 | align-items: center; 553 | justify-content: space-between; 554 | overflow-y: hidden; 555 | overflow-x: auto; 556 | margin: 10px; 557 | } 558 | 559 | .prompt > i { 560 | color: var(--icon-span-color); 561 | font-size: 22px; 562 | cursor: pointer; 563 | margin: 10px; 564 | padding: 10px; 565 | } 566 | .prompt > div { 567 | display: flex; 568 | align-items: center; 569 | justify-content: flex-end; 570 | flex-wrap: nowrap; 571 | } 572 | .prompt > div > p { 573 | width: fit-content; 574 | max-width: 600px; 575 | padding: 4px 8px; 576 | background-color: var(--bg-secondary); 577 | border: 1px solid var(--border-secondary); 578 | border-radius: 15px; 579 | font-family: 'Courier New', Courier, monospace; 580 | font-size: 15px; 581 | white-space: nowrap; 582 | overflow: hidden; 583 | text-overflow: ellipsis; 584 | } 585 | .prompt > div > i { 586 | color: var(--icon-span-color); 587 | font-size: 22px; 588 | padding: 5px; 589 | cursor: pointer; 590 | background-color: var(--border-primary); 591 | border-radius: 50%; 592 | margin: 10px; 593 | margin-left: 5px; 594 | } 595 | 596 | @media screen and (max-width: 768px) { 597 | .prompt > div > p { 598 | max-width: 250px; 599 | } 600 | } 601 | 602 | @keyframes sildein_ttb { 603 | 0% { 604 | top: -100px; 605 | } 606 | 100% { 607 | top: 0; 608 | } 609 | } 610 | 611 | .snackbar { 612 | top: 0; 613 | left: 0; 614 | width: 100%; 615 | height: 75px; 616 | position: fixed; 617 | display: none; 618 | align-items: center; 619 | justify-content: center; 620 | color: var(--white-text); 621 | font-size: 20px; 622 | transition: bottom 0.5s; 623 | z-index: 10001 !important; 624 | background-color: transparent; 625 | animation: sildein_ttb 0.5s; 626 | } 627 | .snackbar > .snack_content { 628 | height: 40px; 629 | max-width: 700px; 630 | min-width: 300px; 631 | width: max-content; 632 | display: flex; 633 | padding: 0 10px; 634 | align-items: center; 635 | justify-content: space-between; 636 | border-radius: 25px; 637 | background-color: rgb(30, 112, 30); 638 | box-shadow: 0 0 10px 0 var(--shadow); 639 | } 640 | .snackbar > .snack_content > p { 641 | font-size: 16px; 642 | text-align: left; 643 | overflow: hidden; 644 | white-space: nowrap; 645 | text-overflow: ellipsis; 646 | } 647 | .snackbar > .snack_content > i { 648 | display: flex; 649 | align-items: center; 650 | justify-content: center; 651 | font-size: 18px; 652 | color: var(--white-text); 653 | padding: 3px; 654 | border-radius: 50%; 655 | background-color: #ffffff3f; 656 | } 657 | @media screen and (max-width: 768px) { 658 | .snackbar > .snack_content { 659 | max-width: 350px; 660 | min-width: 200px; 661 | } 662 | .snackbar > .snack_content > p { 663 | font-size: 14px; 664 | max-width: 200px; 665 | } 666 | .snackbar > .snack_content > i { 667 | font-size: 18px; 668 | } 669 | } 670 | 671 | .queue { 672 | width: 100%; 673 | height: 100%; 674 | display: flex; 675 | flex-direction: column; 676 | justify-content: flex-start; 677 | align-items: center; 678 | padding-top: 10px; 679 | } 680 | .queue > .queue_close { 681 | top: 0; 682 | right: 0; 683 | position: absolute; 684 | width: 25px; 685 | height: 25px; 686 | margin-top: 15px; 687 | margin-right: 20px; 688 | display: flex; 689 | align-items: center; 690 | justify-content: center; 691 | cursor: pointer; 692 | color: var(--icon-span-color); 693 | font-size: 15px; 694 | } 695 | .queue > .queue_content { 696 | width: 100%; 697 | height: 100%; 698 | display: flex; 699 | align-items: center; 700 | flex-direction: column; 701 | justify-content: flex-start; 702 | } 703 | .queue > .queue_content > p { 704 | font-size: 15px; 705 | color: var(--text-color); 706 | margin-bottom: 10px; 707 | padding: 7px 15px; 708 | border-radius: 25px; 709 | } 710 | .queue > .queue_content > ul { 711 | width: 100%; 712 | height: 100%; 713 | padding: 10px 0; 714 | font-size: 20px; 715 | text-align: left; 716 | overflow-y: auto; 717 | border-radius: 5px; 718 | background-color: transparent; 719 | } 720 | .queue > .queue_content > ul > li { 721 | width: 100%; 722 | height: 50px; 723 | padding: 3px 8px 3px 3px; 724 | flex-shrink: 0; 725 | font-size: 20px; 726 | margin-bottom: 8px; 727 | display: flex; 728 | align-items: center; 729 | justify-content: flex-start; 730 | } 731 | .queue > .queue_content > ul > li > p { 732 | width: 60px; 733 | font-size: 13px; 734 | color: var(--text-color); 735 | text-align: center; 736 | border-radius: 5px; 737 | padding-top: 20px; 738 | } 739 | .queue > .queue_content > ul > li > .icon { 740 | font-size: 18px; 741 | color: var(--text-color); 742 | margin: 0 10px; 743 | } 744 | .queue > .queue_content > ul > li > .icon > span { 745 | padding: 10px; 746 | background-color: var(--bg-secondary); 747 | border: 1px solid var(--border-secondary); 748 | border-radius: 5px; 749 | } 750 | .queue > .queue_content > ul > li > .info { 751 | width: 100%; 752 | font-size: 15px; 753 | color: var(--text-color); 754 | display: flex; 755 | align-items: flex-start; 756 | justify-content: center; 757 | flex-direction: column; 758 | } 759 | .queue > .queue_content > ul > li > .info > p { 760 | width: 100%; 761 | max-width: 618px; 762 | font-size: 15px; 763 | color: var(--text-color); 764 | overflow: hidden; 765 | text-overflow: ellipsis; 766 | white-space: nowrap; 767 | margin-bottom: 3px; 768 | } 769 | .queue > .queue_content > ul > li > .info > .progress { 770 | height: 3px; 771 | width: 100%; 772 | border-radius: 10px; 773 | background-color: var(--progress-bar-bg); 774 | } 775 | .queue > .queue_content > ul > li > .info > .progress > .bar { 776 | width: 0; 777 | height: 100%; 778 | background-color: var(--accent-blue); 779 | transition: width 0.5s; 780 | border-radius: 10px; 781 | } 782 | @media screen and (max-width: 768px) { 783 | .queue { 784 | width: 300px; 785 | } 786 | .queue > .queue_close { 787 | width: 20px; 788 | height: 20px; 789 | } 790 | .queue > .queue_content > p { 791 | font-size: 13px; 792 | padding: 5px 10px; 793 | } 794 | .queue > .queue_content > ul { 795 | width: 100%; 796 | height: 100%; 797 | } 798 | .queue > .queue_content > ul > li > .info > p { 799 | max-width: 192px; 800 | } 801 | } 802 | 803 | .file_mover { 804 | width: 100%; 805 | height: 100%; 806 | display: flex; 807 | align-items: center; 808 | justify-content: space-between; 809 | } 810 | .file_mover > button { 811 | display: flex; 812 | align-items: center; 813 | justify-content: center; 814 | border-radius: 5px; 815 | color: var(--text-color); 816 | font-size: 15px; 817 | cursor: pointer; 818 | border: none; 819 | padding: 5px 10px; 820 | background-color: var(--bg-secondary-solid); 821 | } 822 | 823 | .trash_options { 824 | width: 100%; 825 | height: 100%; 826 | display: flex; 827 | align-items: center; 828 | justify-content: space-between; 829 | } 830 | .trash_options > button { 831 | display: flex; 832 | align-items: center; 833 | justify-content: center; 834 | border-radius: 5px; 835 | color: var(--text-color); 836 | font-size: 18px; 837 | cursor: pointer; 838 | border: none; 839 | padding: 10px; 840 | background-color: rgb(241, 61, 61); 841 | } 842 | @media screen and (max-width: 768px) { 843 | .trash_options > button { 844 | font-size: 15px; 845 | padding: 8px; 846 | } 847 | } 848 | 849 | .file_sender { 850 | width: 100%; 851 | height: 100%; 852 | display: flex; 853 | flex-direction: column; 854 | justify-content: flex-start; 855 | align-items: center; 856 | padding: 45px; 857 | } 858 | .file_sender > input[type="text"] { 859 | width: 100%; 860 | margin-top: 10px; 861 | margin-bottom: 20px; 862 | outline: none; 863 | border: none; 864 | border-radius: 5px; 865 | font-size: 18px; 866 | padding: 5px 10px; 867 | color: #ccc; 868 | background-color: var(--bg-secondary-solid); 869 | } 870 | .file_sender > div > button { 871 | border: none; 872 | padding: 7px 20px; 873 | font-size: 16px; 874 | color: var(--text-color); 875 | cursor: pointer; 876 | outline: none; 877 | white-space: nowrap; 878 | border-radius: 5px; 879 | flex-shrink: 0; 880 | background-color: var(--bg-secondary-solid); 881 | margin: 0 10px; 882 | } 883 | .file_sender > p { 884 | width: 100%; 885 | max-width: 450px; 886 | color: #ccc; 887 | font-size: 16px; 888 | flex-shrink: 0; 889 | text-align: left; 890 | white-space: nowrap; 891 | overflow: hidden; 892 | text-overflow: ellipsis; 893 | } 894 | @media screen and (max-width: 768px) { 895 | .file_sender { 896 | padding: 20px; 897 | } 898 | .file_sender > input[type="text"] { 899 | font-size: 13px; 900 | } 901 | .file_sender > div > button { 902 | font-size: 13px; 903 | } 904 | .file_sender > p { 905 | font-size: 13px; 906 | max-width: 250px; 907 | } 908 | } 909 | 910 | .context_menu { 911 | width: 260px; 912 | height: max-content; 913 | position: fixed; 914 | border-radius: 5px; 915 | color: var(--text-color); 916 | padding: 10px; 917 | display: none; 918 | align-items: center; 919 | justify-content: center; 920 | flex-direction: column; 921 | box-shadow: 0 0 10px 0 var(--shadow); 922 | background-color: var(--bg-primary); 923 | z-index: 9999 !important; 924 | border: 1px solid var(--bg-secondary-solid); 925 | } 926 | .context_menu > ul { 927 | width: 100%; 928 | height: 100%; 929 | display: flex; 930 | font-size: 15px; 931 | align-items: center; 932 | justify-content: center; 933 | flex-direction: column; 934 | } 935 | .context_menu > ul > li { 936 | width: 100%; 937 | height: 40px; 938 | display: flex; 939 | align-items: center; 940 | justify-content: space-between; 941 | padding: 0 10px; 942 | cursor: pointer; 943 | transition: 0.2s; 944 | border-radius: 5px; 945 | } 946 | .context_menu > ul > li:hover { 947 | background-color: var(--border-primary); 948 | } 949 | .context_menu > ul > li > span { 950 | font-size: 18px; 951 | color: var(--icon-span-color); 952 | } 953 | 954 | .greetings { 955 | width: 100vw; 956 | height: 100vh; 957 | height: 100dvh; 958 | display: flex; 959 | align-items: center; 960 | justify-content: center; 961 | background-color: var(--bg-primary); 962 | color: var(--text-color); 963 | overflow-y: auto; 964 | padding: 40px; 965 | } 966 | .greetings > .skip { 967 | position: absolute; 968 | top: 10px; 969 | right: 10px; 970 | width: max-content; 971 | height: max-content; 972 | display: flex; 973 | align-items: center; 974 | justify-content: space-between; 975 | cursor: pointer; 976 | border-radius: 15px; 977 | font-size: 15px; 978 | transition: 0.2s; 979 | color: #ccc; 980 | padding: 2px 15px; 981 | background-color: var(--border-primary); 982 | } 983 | .greetings > .inner { 984 | width: 100%; 985 | height: 100%; 986 | display: flex; 987 | align-items: center; 988 | justify-content: center; 989 | flex-direction: column; 990 | } 991 | .greetings > .inner > * { 992 | padding: 10px; 993 | } 994 | .greetings > .inner > h1 { 995 | font-size: 30px; 996 | color: #ccc; 997 | } 998 | .greetings > .inner > img { 999 | width: 320px; 1000 | height: 320px; 1001 | border-radius: 20px; 1002 | } 1003 | .greetings > .inner > .drop { 1004 | width: 250px; 1005 | height: 150px; 1006 | border-radius: 10px; 1007 | display: flex; 1008 | align-items: center; 1009 | justify-content: center; 1010 | border: 2px dashed var(--border-primary); 1011 | cursor: pointer; 1012 | } 1013 | .greetings > .inner > .drop > span { 1014 | font-size: 15px; 1015 | color: #ccc; 1016 | text-align: center; 1017 | cursor: pointer; 1018 | max-width: 200px; 1019 | overflow: hidden; 1020 | text-overflow: ellipsis; 1021 | } 1022 | .greetings > .inner > button { 1023 | width: 100%; 1024 | max-width: 250px; 1025 | height: 40px; 1026 | border: none; 1027 | outline: none; 1028 | border-radius: 5px; 1029 | font-size: 16px; 1030 | color: var(--text-color); 1031 | margin-top: 20px; 1032 | cursor: pointer; 1033 | transition: 0.2s; 1034 | background-color: var(--accent-blue); 1035 | } 1036 | @media screen and (max-width: 768px) { 1037 | .greetings { 1038 | padding: 0; 1039 | flex-direction: column; 1040 | } 1041 | .greetings > .inner > h1 { 1042 | font-size: 23px; 1043 | } 1044 | .greetings > .inner > img { 1045 | width: 200px; 1046 | height: 200px; 1047 | } 1048 | .greetings > .inner > .drop { 1049 | width: 200px; 1050 | height: 100px; 1051 | } 1052 | .greetings > .inner > .drop > span { 1053 | font-size: 13px; 1054 | } 1055 | .greetings > .inner > button { 1056 | max-width: 200px; 1057 | } 1058 | } 1059 | 1060 | .file_migration { 1061 | width: 100vw; 1062 | height: 100vh; 1063 | margin: auto; 1064 | padding: 15px; 1065 | position: fixed; 1066 | color: var(--text-color); 1067 | border-radius: 10px; 1068 | background-color: var(--bg-primary); 1069 | border: 1px solid var(--bg-secondary-solid); 1070 | display: none; 1071 | align-items: center; 1072 | justify-content: center; 1073 | overflow-y: auto; 1074 | } 1075 | 1076 | .file_migration::backdrop { 1077 | background-color: rgba(0, 0, 0, 0.692); 1078 | } 1079 | 1080 | .file_migration p { 1081 | width: 100%; 1082 | background: #ffffff1a; 1083 | padding: 5px 15px; 1084 | margin: 4px 0; 1085 | border-radius: 5px; 1086 | overflow-wrap: anywhere; 1087 | } 1088 | .file_migration h3 { 1089 | margin-bottom: 10px; 1090 | } 1091 | 1092 | .file_migration button { 1093 | position: absolute; 1094 | right: 0; 1095 | margin-right: 10px; 1096 | padding: 5px; 1097 | border: none; 1098 | outline: none; 1099 | border-radius: 50%; 1100 | cursor: pointer; 1101 | transition: 0.2s; 1102 | background-color: var(--bg-secondary); 1103 | color: var(--icon-span-color); 1104 | display: flex; 1105 | } 1106 | 1107 | .file_preview { 1108 | width: fit-content; 1109 | min-width: 328px; 1110 | height: fit-content; 1111 | margin: auto; 1112 | padding: 15px; 1113 | position: fixed; 1114 | color: var(--text-color); 1115 | border-radius: 30px; 1116 | background-color: var(--bg-primary); 1117 | border: 1px solid var(--bg-secondary-solid); 1118 | display: none; 1119 | align-items: center; 1120 | justify-content: center; 1121 | } 1122 | .file_preview::backdrop { 1123 | background-color: rgba(0, 0, 0, 0.692); 1124 | } 1125 | .file_preview > p { 1126 | width: 100%; 1127 | padding-left: 10px; 1128 | margin-right: 20px; 1129 | font-size: 16px; 1130 | word-break: break-all; 1131 | } 1132 | .file_preview > span { 1133 | padding: 5px; 1134 | background-color: var(--bg-secondary); 1135 | border: 1px solid var(--border-secondary); 1136 | border-radius: 50%; 1137 | pointer-events: none; 1138 | cursor: pointer; 1139 | color: var(--icon-span-color); 1140 | font-size: 19px; 1141 | margin-left: 10px; 1142 | } 1143 | 1144 | .rename { 1145 | width: max-content; 1146 | height: max-content; 1147 | display: flex; 1148 | align-items: center; 1149 | justify-content: center; 1150 | flex-direction: column; 1151 | padding: 8px; 1152 | border-radius: 10px; 1153 | } 1154 | .rename input { 1155 | width: 100%; 1156 | height: 40px; 1157 | border: none; 1158 | outline: none; 1159 | border-radius: 40px; 1160 | font-size: 16px; 1161 | color: var(--text-color); 1162 | padding: 10px 20px; 1163 | background-color: var(--bg-secondary); 1164 | border: 1px solid var(--border-secondary); 1165 | } 1166 | .rename button { 1167 | padding: 10px 20px; 1168 | border: none; 1169 | outline: none; 1170 | border-radius: 25px; 1171 | font-size: 15px; 1172 | color: var(--text-color); 1173 | margin-top: 20px; 1174 | cursor: pointer; 1175 | transition: 0.2s; 1176 | background-color: var(--accent-blue); 1177 | } -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "backend/deta" 5 | "crypto/rand" 6 | "encoding/hex" 7 | "fmt" 8 | "os" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | func MatchProjectId(id string) bool { 14 | return strings.HasPrefix(os.Getenv("DETA_API_KEY"), id) 15 | } 16 | 17 | func FileToDriveSavedName(file map[string]interface{}) string { 18 | hash := file["hash"].(string) 19 | fragment := strings.Split(file["name"].(string), ".") 20 | var driveFilename string 21 | if len(fragment) > 1 { 22 | driveFilename = fmt.Sprintf("%s.%s", hash, fragment[len(fragment)-1]) 23 | } else { 24 | driveFilename = hash 25 | } 26 | return driveFilename 27 | } 28 | 29 | func FolderToAsParentPath(folder map[string]interface{}) string { 30 | var path string 31 | parent, ok := folder["parent"] 32 | if ok && parent != nil { 33 | path = parent.(string) + "/" + folder["name"].(string) 34 | } else { 35 | path = folder["name"].(string) 36 | } 37 | return path 38 | } 39 | 40 | func randomHex(n int) string { 41 | b := make([]byte, n) 42 | if _, err := rand.Read(b); err != nil { 43 | panic(err) 44 | } 45 | return hex.EncodeToString(b) 46 | } 47 | 48 | type FileV1 struct { 49 | Key string `json:"key"` 50 | Access string `json:"access"` 51 | Date string `json:"date"` 52 | Hash string `json:"hash"` 53 | Mime string `json:"mime"` 54 | Name string `json:"name"` 55 | Color string `json:"color"` 56 | Parent string `json:"parent"` 57 | Size float64 `json:"size"` 58 | Type string `json:"type"` 59 | Pinned bool `json:"pinned"` 60 | Recipients []string `json:"recipients"` 61 | Deleted bool `json:"deleted"` 62 | } 63 | 64 | type AccessToken struct { 65 | Value string `json:"value"` 66 | Label string `json:"label"` 67 | } 68 | 69 | type FileV2 struct { 70 | Key string `json:"key"` 71 | Name string `json:"name"` 72 | Color string `json:"color"` 73 | NameLowercase string `json:"nameLowercase"` 74 | Folder bool `json:"folder"` 75 | Public bool `json:"public"` 76 | CreatedAt string `json:"createdAt"` 77 | Path string `json:"path"` 78 | Type string `json:"type"` 79 | Owner string `json:"owner"` 80 | Size float64 `json:"size"` 81 | Tag []string `json:"tag"` 82 | Partial bool `json:"partial"` 83 | UploadedUpTo float64 `json:"uploadedUpTo"` 84 | Deleted bool `json:"deleted"` 85 | AccessTokens []AccessToken `json:"accessTokens"` 86 | } 87 | 88 | func buildFolderQuery(name string, parent interface{}) *deta.Query { 89 | q := deta.NewQuery() 90 | q.Limit = 1 91 | q.Equals("name", name) 92 | q.Equals("parent", parent) 93 | q.Equals("type", "folder") 94 | return q 95 | } 96 | 97 | func buildPathV2FromV1(parent string) string { 98 | if parent == "" { 99 | return "/" 100 | } 101 | var path string 102 | expr, _ := regexp.Compile(`[^/]+`) 103 | matches := expr.FindAllString(parent, -1) 104 | rootFolder := matches[0] 105 | resp := base.Fetch(buildFolderQuery(rootFolder, nil)) 106 | folderData := resp.JSON()["items"].([]interface{}) 107 | if len(folderData) == 0 { 108 | return "/" 109 | } 110 | folder := folderData[0].(map[string]interface{}) 111 | path += fmt.Sprintf("/%s", folder["hash"].(string)) 112 | matches = matches[1:] 113 | if len(matches) == 0 { 114 | return path 115 | } 116 | folderNames := matches 117 | matches = matches[:len(matches)-1] 118 | subParents := []string{rootFolder} 119 | tmp := rootFolder 120 | for _, match := range matches { 121 | tmp += "/" + match 122 | subParents = append(subParents, tmp) 123 | } 124 | for i, subParent := range subParents { 125 | name := folderNames[i] 126 | resp := base.Fetch(buildFolderQuery(name, subParent)) 127 | folderData := resp.JSON()["items"].([]interface{}) 128 | folder = folderData[0].(map[string]interface{}) 129 | if len(folderData) == 0 { 130 | return "/" 131 | } 132 | path += fmt.Sprintf("/%s", folder["hash"].(string)) 133 | } 134 | return path 135 | } 136 | -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js'); 2 | 3 | workbox.routing.registerRoute( 4 | /\.(?:png|gif|jpg|jpeg|svg)$/, 5 | new workbox.strategies.CacheFirst(), 6 | ); 7 | 8 | workbox.routing.registerRoute( 9 | /.*(?:googleapis|gstatic)\.com.*$/, 10 | new workbox.strategies.StaleWhileRevalidate(), 11 | ); 12 | 13 | workbox.routing.registerRoute( 14 | /\.(?:js|css)$/, 15 | new workbox.strategies.NetworkOnly(), 16 | ); 17 | --------------------------------------------------------------------------------