From 5ecd9a6e97378c0480555415356c70ffaf5988b5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 31 May 2023 14:30:13 +0200 Subject: [PATCH 1/6] ui: Add interface to manage integrations --- internal/ui/handlers.go | 139 ++++++++++++++ internal/ui/routes.go | 7 + ui/src/App.jsx | 2 + ui/src/components/Navigation.js | 5 + ui/src/pages/Integrations/IntegrationModal.js | 172 ++++++++++++++++++ .../pages/Integrations/NewIntegrationModal.js | 153 ++++++++++++++++ ui/src/pages/Integrations/index.jsx | 116 ++++++++++++ ui/src/services/api.service.js | 30 +++ 8 files changed, 624 insertions(+) create mode 100644 ui/src/pages/Integrations/IntegrationModal.js create mode 100644 ui/src/pages/Integrations/NewIntegrationModal.js create mode 100644 ui/src/pages/Integrations/index.jsx diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index 7a32f406..e30e7823 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -20,6 +20,7 @@ const ( browserIDContextKey = "browserID" isSync15Key = "sync15" docIDParam = "docid" + intIDParam = "intid" uiLogger = "[ui] " ui10 = " [10] " useridParam = "userid" @@ -509,3 +510,141 @@ func (app *ReactAppWrapper) createUser(c *gin.Context) { } c.Status(http.StatusCreated) } + +func (app *ReactAppWrapper) listIntegrations(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + c.JSON(http.StatusOK, user.Integrations) +} + +func (app *ReactAppWrapper) createIntegration(c *gin.Context) { + int := model.IntegrationConfig{} + if err := c.ShouldBindJSON(&int); err != nil { + log.Error(err) + badReq(c, err.Error()) + return + } + + uid := c.GetString(userIDContextKey) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + int.ID = uuid.NewString() + user.Integrations = append(user.Integrations, int) + + err = app.userStorer.UpdateUser(user) + + if err != nil { + log.Error("error updating user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, int) +} + +func (app *ReactAppWrapper) getIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + intid := common.ParamS(intIDParam, c) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + for _, integration := range user.Integrations { + if integration.ID == intid { + c.JSON(http.StatusOK, integration) + return + } + } + + c.AbortWithStatus(http.StatusNotFound) +} + +func (app *ReactAppWrapper) updateIntegration(c *gin.Context) { + int := model.IntegrationConfig{} + if err := c.ShouldBindJSON(&int); err != nil { + log.Error(err) + badReq(c, err.Error()) + return + } + + uid := c.GetString(userIDContextKey) + + intid := common.ParamS(intIDParam, c) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + for idx, integration := range user.Integrations { + if integration.ID == intid { + int.ID = integration.ID + user.Integrations[idx] = int + + err = app.userStorer.UpdateUser(user) + + if err != nil { + log.Error("error updating user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, int) + return + } + } + + c.AbortWithStatus(http.StatusNotFound) +} + +func (app *ReactAppWrapper) deleteIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + intid := common.ParamS(intIDParam, c) + + user, err := app.userStorer.GetUser(uid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusUnauthorized) + return + } + + for idx, integration := range user.Integrations { + if integration.ID == intid { + user.Integrations = append(user.Integrations[:idx], user.Integrations[idx+1:]...) + + err = app.userStorer.UpdateUser(user) + + if err != nil { + log.Error("error updating user", err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.Status(http.StatusAccepted) + return + } + } + + c.AbortWithStatus(http.StatusNotFound) +} diff --git a/internal/ui/routes.go b/internal/ui/routes.go index 718efaaa..4df7fd14 100644 --- a/internal/ui/routes.go +++ b/internal/ui/routes.go @@ -72,6 +72,13 @@ func (app *ReactAppWrapper) RegisterRoutes(router *gin.Engine) { auth.POST("folders", app.createFolder) auth.GET("documents/:docid/metadata", app.getDocumentMetadata) + // integrations + auth.GET("integrations", app.listIntegrations) + auth.POST("integrations", app.createIntegration) + auth.GET("integrations/:intid", app.getIntegration) + auth.PUT("integrations/:intid", app.updateIntegration) + auth.DELETE("integrations/:intid", app.deleteIntegration) + //admin admin := auth.Group("") admin.Use(app.adminMiddleware()) diff --git a/ui/src/App.jsx b/ui/src/App.jsx index 67fe8997..e764c669 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -12,6 +12,7 @@ import Login from "./pages/Login"; import Home from "./pages/Home"; import Connect from "./pages/Connect"; import Documents from "./pages/Documents"; +import Integrations from "./pages/Integrations"; import Profile from "./pages/Profile"; import Admin from "./pages/Admin"; import NoMatch from "./pages/404"; @@ -35,6 +36,7 @@ export default function App() { + diff --git a/ui/src/components/Navigation.js b/ui/src/components/Navigation.js index cc1c4aa4..4607efc5 100644 --- a/ui/src/components/Navigation.js +++ b/ui/src/components/Navigation.js @@ -33,6 +33,11 @@ const NavigationBar = () => { Documents + + + Integrations + + Connect diff --git a/ui/src/pages/Integrations/IntegrationModal.js b/ui/src/pages/Integrations/IntegrationModal.js new file mode 100644 index 00000000..a2f9d156 --- /dev/null +++ b/ui/src/pages/Integrations/IntegrationModal.js @@ -0,0 +1,172 @@ +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import { Button, Card } from "react-bootstrap"; +import apiService from "../../services/api.service"; + +import { Alert } from "react-bootstrap"; + +export default function IntegrationModal(params) { + const { integration, onSave, headerText, onClose } = params; + + const [formErrors, setFormErrors] = useState({}); + const [integrationForm, setIntegrationForm] = useState({ + name: integration?.Name, + provider: integration?.Provider, + email: integration?.email, + username: integration?.Username, + password: integration?.Password, + address: integration?.Address, + insecure: integration?.Insecure, + accesstoken: integration?.Accesstoken, + path: integration?.Path, + }); + + function handleChange({ target }) { + setIntegrationForm({ ...integrationForm, [target.name]: target.value }); + } + + function formIsValid() { + const _errors = {}; + + if (!integrationForm.name) _errors.error = "name is required"; + + setFormErrors(_errors); + + return Object.keys(_errors).length === 0; + } + + async function handleSubmit(event) { + event.preventDefault(); + + if (!formIsValid()) return; + + try { + await apiService.updateintegration({ + id: integration.ID, + name: integrationForm.name, + provider: integrationForm.provider, + username: integrationForm.username, + password: integrationForm.password, + address: integrationForm.address, + insecure: integrationForm.insecure, + accesstoken: integrationForm.accesstoken, + path: integrationForm.path, + }); + onSave(); + } catch (e) { + setFormErrors({ error: e.toString() }); + } + } + + if (!integration) return null; + return ( +
+ + + {headerText} + + +
+ + + IntegrationID + + + Provider + + + + + + + Name + + + {integrationForm.provider === "webdav" && ( + <> + Address + + + )} + {integrationForm.provider === "webdav" && ( + <> + Username + + + )} + {integrationForm.provider === "webdav" && ( + <> + Password + + + )} + + {integrationForm.provider === "localfs" && ( + <> + Path + + + )} + + {integrationForm.provider === "dropbox" && ( + <> + Access Token + + + )} +
+
+ + + + +
+
+ ); +} diff --git a/ui/src/pages/Integrations/NewIntegrationModal.js b/ui/src/pages/Integrations/NewIntegrationModal.js new file mode 100644 index 00000000..7490f37b --- /dev/null +++ b/ui/src/pages/Integrations/NewIntegrationModal.js @@ -0,0 +1,153 @@ +import React, { useState } from "react"; +import Form from "react-bootstrap/Form"; +import { Button, Card } from "react-bootstrap"; +import apiService from "../../services/api.service"; + +import { Alert } from "react-bootstrap"; + +export default function IntegrationProfileModal(params) { + const { onSave, onClose } = params; + + const [formErrors, setFormErrors] = useState({}); + const [formInfo, setFormInfo] = useState({}); + const [integrationForm, setIntegrationForm] = useState({ + name: "", + provider: "localfs", + }); + + function handleChange({ target }) { + setIntegrationForm({ ...integrationForm, [target.name]: target.value }); + } + + function formIsValid() { + const _errors = {}; + + if (!integrationForm.name) _errors.error = "name is required"; + + if (!integrationForm.provider) _errors.error = "provider is required"; + + setFormErrors(_errors); + + return Object.keys(_errors).length === 0; + } + + async function handleSubmit(event) { + event.preventDefault(); + + if (!formIsValid()) return; + + try { + await apiService.createintegration(integrationForm); + setFormInfo({ message: "Created" }); + onSave(); + } catch (e) { + setFormErrors({ error: e.toString() }); + } + } + + return ( +
+ + + New Integration + + + + + + + Name + + + Provider + + + + + + + {integrationForm.provider === "webdav" && ( + <> + Address + + + )} + {integrationForm.provider === "webdav" && ( + <> + Username + + + )} + {integrationForm.provider === "webdav" && ( + <> + Password + + + )} + + {integrationForm.provider === "localfs" && ( + <> + Path + + + )} + + {integrationForm.provider === "dropbox" && ( + <> + Access Token + + + )} + + + + + + +
+ ); +} diff --git a/ui/src/pages/Integrations/index.jsx b/ui/src/pages/Integrations/index.jsx new file mode 100644 index 00000000..1c94cc75 --- /dev/null +++ b/ui/src/pages/Integrations/index.jsx @@ -0,0 +1,116 @@ +import React, {useState} from "react"; +import useFetch from "../../hooks/useFetch"; +import Spinner from "../../components/Spinner"; +import {Alert, Button, Card, Container, Modal, Table} from "react-bootstrap"; +import IntegrationModal from "./IntegrationModal"; +import NewIntegrationModal from "./NewIntegrationModal"; +import apiService from "../../services/api.service"; +import { toast } from "react-toastify"; +const integrationListUrl = "integrations"; + +const NewIntegration = 1; +const UpdateIntegration = 2; +const Integrations = () => { + const [index, setIndex] = useState(0); + const { data: integrationList, error, loading } = useFetch(`${integrationListUrl}`, index); + const [ state, setState ] = useState({showModal: 0, modalIntegration: null}); + const refresh = () =>{ + setIndex(previous => previous+1) + } + + function openModal(index: number) { + if (!integrationList) return; + let integration = integrationList[index]; + setState({ + showModal: UpdateIntegration, + modalIntegration: integration, + }); + } + function closeModal() { + setState({ + showModal: 0, + modalIntegration: null, + }); + } + + if (loading) { + return + } + + if (error) { + return ( + + An Error Occurred + {`Error ${error.status}: ${error.statusText}`} + + ); + } + + const newIntegration = e => { + setState({ + showModal: NewIntegration, + }); + } + + const onSave = () => { + closeModal(); + refresh(); + } + + const remove = async (e, id, name) => { + e.preventDefault() + e.stopPropagation() + if (!window.confirm(`Are you sure you want to delete integration: ${name}?`)) + return false + + try{ + await apiService.deleteintegration(id) + refresh() + } catch(e){ + toast.error('Error:'+ e) + } + } + + return ( + +

Integrations

+ + + + + + + + + + + + + {!integrationList.length && ( + + + + )} + {integrationList.map((i, index) => ( + openModal(index)} style={{ cursor: "pointer" }}> + + + + + + + ))} + +
#IntegrationIdNameProvider
No integration
{index}{i.ID}{i.Name}{i.Provider}
+ + + + + + +
+
+ ); +}; + +export default Integrations; diff --git a/ui/src/services/api.service.js b/ui/src/services/api.service.js index 7fde7925..a8a07ef5 100644 --- a/ui/src/services/api.service.js +++ b/ui/src/services/api.service.js @@ -127,6 +127,36 @@ class ApiServices { headers: this.header(), }).then((r) => handleError(r)); } + + listintegration() { + return fetch(`${constants.ROOT_URL}/integrations`, { + method: "GET", + headers: this.header(), + }).then((r) => { + handleError(r); + return r.json(); + }); + } + updateintegration(integration) { + return fetch(`${constants.ROOT_URL}/integrations/${integration.id}`, { + method: "PUT", + headers: this.header(), + body: JSON.stringify(integration), + }).then((r) => handleError(r)); + } + createintegration(integration) { + return fetch(`${constants.ROOT_URL}/integrations`, { + method: "POST", + headers: this.header(), + body: JSON.stringify(integration), + }).then((r) => handleError(r)); + } + deleteintegration(integrationid) { + return fetch(`${constants.ROOT_URL}/integrations/${integrationid}`, { + method: "DELETE", + headers: this.header(), + }).then((r) => handleError(r)); + } } function removeUser(){ From 2937e8eacb102435873fcc3cd45672b92628de39 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Wed, 31 May 2023 14:34:15 +0200 Subject: [PATCH 2/6] Reorder providers --- internal/integrations/integrations.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/integrations/integrations.go b/internal/integrations/integrations.go index a7f4986c..b4f59dc5 100644 --- a/internal/integrations/integrations.go +++ b/internal/integrations/integrations.go @@ -37,12 +37,12 @@ func GetIntegrationProvider(storer storage.UserStorer, uid, integrationid string continue } switch intg.Provider { - case webdavProvider: - return newWebDav(intg), nil case dropboxProvider: return newDropbox(intg), nil case localfsProvider: return newLocalFS(intg), nil + case webdavProvider: + return newWebDav(intg), nil } } return nil, fmt.Errorf("integration not found or no implmentation (only webdav) %s", integrationid) From 1eaa6a06cb82e9a80bb53821dbab6cf05c42f0a5 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 17 Nov 2024 15:03:56 +0100 Subject: [PATCH 3/6] Implement metadata retrieval for integrations --- internal/app/handlers.go | 22 +++++++++++++++++++--- internal/integrations/dropbox.go | 5 +++++ internal/integrations/integrations.go | 1 + internal/integrations/localfs.go | 19 +++++++++++++++++++ internal/integrations/webdav.go | 19 +++++++++++++++++++ internal/messages/messages.go | 11 +++++++---- 6 files changed, 70 insertions(+), 7 deletions(-) diff --git a/internal/app/handlers.go b/internal/app/handlers.go index 4773eb96..e7e76a4b 100644 --- a/internal/app/handlers.go +++ b/internal/app/handlers.go @@ -908,9 +908,25 @@ func (app *App) blobStorageWrite(c *gin.Context) { } func (app *App) integrationsGetMetadata(c *gin.Context) { - var metadata messages.IntegrationMetadata - metadata.Thumbnail = "" - c.JSON(http.StatusOK, &metadata) + uid := c.GetString(userIDKey) + integrationID := common.ParamS(integrationKey, c) + fileID := common.ParamS(fileKey, c) + + integrationProvider, err := integrations.GetIntegrationProvider(app.userStorer, uid, integrationID) + if err != nil { + log.Error(fmt.Errorf("can't get integration, %v", err)) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + metadata, err := integrationProvider.GetMetadata(fileID) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, metadata) } func (app *App) integrationsUpload(c *gin.Context) { diff --git a/internal/integrations/dropbox.go b/internal/integrations/dropbox.go index 41f7113f..7e06f0d8 100644 --- a/internal/integrations/dropbox.go +++ b/internal/integrations/dropbox.go @@ -24,6 +24,11 @@ func newDropbox(i model.IntegrationConfig) *DropBox { } } +func (d *DropBox) GetMetadata(fileID string) (*messages.IntegrationMetadata, error) { + // TODO + return nil, nil +} + func (d *DropBox) List(folderID string, depth int) (*messages.IntegrationFolder, error) { args := files.ListFolderArg{ diff --git a/internal/integrations/integrations.go b/internal/integrations/integrations.go index b4f59dc5..79ddfbe0 100644 --- a/internal/integrations/integrations.go +++ b/internal/integrations/integrations.go @@ -21,6 +21,7 @@ const ( // IntegrationProvider abstracts 3rd party integrations type IntegrationProvider interface { + GetMetadata(fileID string) (result *messages.IntegrationMetadata, err error) List(folderID string, depth int) (result *messages.IntegrationFolder, err error) Download(fileID string) (io.ReadCloser, error) Upload(folderID, name, fileType string, reader io.ReadCloser) (string, error) diff --git a/internal/integrations/localfs.go b/internal/integrations/localfs.go index b194619d..884fab73 100644 --- a/internal/integrations/localfs.go +++ b/internal/integrations/localfs.go @@ -26,6 +26,25 @@ func newLocalFS(i model.IntegrationConfig) *localFS { } } +func (d *localFS) GetMetadata(fileID string) (*messages.IntegrationMetadata, error) { + decoded, err := decodeName(fileID) + if err != nil { + return nil, err + } + + ext := path.Ext(decoded) + contentType := contentTypeFromExt(ext) + + return &messages.IntegrationMetadata{ + ID: fileID, + Name: path.Base(decoded), + Thumbnail: []byte{}, + SourceFileType: contentType, + ProvidedFileType: contentType, + FileType: ext, + }, nil +} + // List populates the response func (d *localFS) List(folder string, depth int) (*messages.IntegrationFolder, error) { response := messages.NewIntegrationFolder(folder, "") diff --git a/internal/integrations/webdav.go b/internal/integrations/webdav.go index 74ab2900..6584f979 100644 --- a/internal/integrations/webdav.go +++ b/internal/integrations/webdav.go @@ -97,6 +97,25 @@ func (w *WebDavIntegration) Download(fileID string) (io.ReadCloser, error) { return w.c.ReadStream(decoded) } +func (w *WebDavIntegration) GetMetadata(fileID string) (*messages.IntegrationMetadata, error) { + decoded, err := decodeName(fileID) + if err != nil { + return nil, err + } + + ext := path.Ext(decoded) + contentType := contentTypeFromExt(ext) + + return &messages.IntegrationMetadata{ + ID: fileID, + Name: path.Base(decoded), + Thumbnail: []byte{}, + SourceFileType: contentType, + ProvidedFileType: contentType, + FileType: ext, + }, nil +} + // List populates the response func (w *WebDavIntegration) List(folder string, depth int) (*messages.IntegrationFolder, error) { response := messages.NewIntegrationFolder(folder, "") diff --git a/internal/messages/messages.go b/internal/messages/messages.go index 87651ff6..77985ed0 100644 --- a/internal/messages/messages.go +++ b/internal/messages/messages.go @@ -225,8 +225,11 @@ type IntegrationFolder struct { } type IntegrationMetadata struct { - FileType string `json:"fileType"` - ID string `json:"id"` - Name string `json:"name"` - Thumbnail string `json:"thumbnail"` + ID string `json:"id"` + Name string `json:"name"` + // Thumbnail is base64 encoded string of an image/png + Thumbnail []byte `json:"thumbnail"` + SourceFileType string `json:"sourceFileType"` + ProvidedFileType string `json:"providedFileType"` + FileType string `json:"fileType"` } From adefc34943400e5dcf9152d73bcebb7678ec373c Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 17 Nov 2024 15:05:05 +0100 Subject: [PATCH 4/6] integrations: Add Size in return to download interface --- internal/app/handlers.go | 5 +++-- internal/integrations/dropbox.go | 4 ++-- internal/integrations/integrations.go | 3 +-- internal/integrations/localfs.go | 13 ++++++++++--- internal/integrations/webdav.go | 14 +++++++++++--- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/internal/app/handlers.go b/internal/app/handlers.go index e7e76a4b..0557c831 100644 --- a/internal/app/handlers.go +++ b/internal/app/handlers.go @@ -968,7 +968,7 @@ func (app *App) integrationsGetFile(c *gin.Context) { return } - reader, err := integrationProvider.Download(fileID) + reader, size, err := integrationProvider.Download(fileID) if err != nil { log.Error(err) c.AbortWithStatus(http.StatusInternalServerError) @@ -977,8 +977,9 @@ func (app *App) integrationsGetFile(c *gin.Context) { defer reader.Close() - c.DataFromReader(http.StatusOK, -1, "application/octet-stream", reader, nil) + c.DataFromReader(http.StatusOK, size, "application/octet-stream", reader, nil) } + func (app *App) integrationsList(c *gin.Context) { uid := c.GetString(userIDKey) integrationID := common.ParamS(integrationKey, c) diff --git a/internal/integrations/dropbox.go b/internal/integrations/dropbox.go index 7e06f0d8..0e539eb6 100644 --- a/internal/integrations/dropbox.go +++ b/internal/integrations/dropbox.go @@ -70,8 +70,8 @@ func (d *DropBox) List(folderID string, depth int) (*messages.IntegrationFolder, return response, nil } -func (d *DropBox) Download(fileID string) (io.ReadCloser, error) { - return nil, nil +func (d *DropBox) Download(fileID string) (io.ReadCloser, int64, error) { + return nil, 0, nil } func (d *DropBox) Upload(folderID, name, fileType string, reader io.ReadCloser) (string, error) { diff --git a/internal/integrations/integrations.go b/internal/integrations/integrations.go index 79ddfbe0..0dfe29d0 100644 --- a/internal/integrations/integrations.go +++ b/internal/integrations/integrations.go @@ -23,7 +23,7 @@ const ( type IntegrationProvider interface { GetMetadata(fileID string) (result *messages.IntegrationMetadata, err error) List(folderID string, depth int) (result *messages.IntegrationFolder, err error) - Download(fileID string) (io.ReadCloser, error) + Download(fileID string) (io.ReadCloser, int64, error) Upload(folderID, name, fileType string, reader io.ReadCloser) (string, error) } @@ -126,7 +126,6 @@ func visitDir(root, currentPath string, depth int, parentFolder *messages.Integr docName := strings.TrimSuffix(entryName, ext) extension := strings.TrimPrefix(ext, ".") - file := &messages.IntegrationFile{ ProvidedFileType: contentType, DateChanged: d.ModTime(), diff --git a/internal/integrations/localfs.go b/internal/integrations/localfs.go index 884fab73..2470903c 100644 --- a/internal/integrations/localfs.go +++ b/internal/integrations/localfs.go @@ -88,16 +88,23 @@ func (d *localFS) List(folder string, depth int) (*messages.IntegrationFolder, e return response, nil } -func (d *localFS) Download(fileID string) (io.ReadCloser, error) { +func (d *localFS) Download(fileID string) (io.ReadCloser, int64, error) { decoded, err := decodeName(fileID) if err != nil { - return nil, err + return nil, 0, err } localPath := path.Join(d.rootPath, path.Clean(decoded)) - return os.Open(localPath) + st, err := os.Stat(localPath) + if err != nil { + return nil, 0, err + } + + res, err := os.Open(localPath) + return res, st.Size(), err } + func (d *localFS) Upload(folderID, name, fileType string, reader io.ReadCloser) (id string, err error) { folder := "/" if folderID != rootFolder { diff --git a/internal/integrations/webdav.go b/internal/integrations/webdav.go index 6584f979..267966d1 100644 --- a/internal/integrations/webdav.go +++ b/internal/integrations/webdav.go @@ -89,12 +89,20 @@ func (w *WebDavIntegration) Upload(folderID, name, fileType string, reader io.Re } // Download downloads -func (w *WebDavIntegration) Download(fileID string) (io.ReadCloser, error) { +func (w *WebDavIntegration) Download(fileID string) (io.ReadCloser, int64, error) { decoded, err := decodeName(fileID) if err != nil { - return nil, err + return nil, 0, err + } + + st, err := w.c.Stat(decoded) + if err != nil { + return nil, 0, err } - return w.c.ReadStream(decoded) + + res, err := w.c.ReadStream(decoded) + + return res, st.Size(), err } func (w *WebDavIntegration) GetMetadata(fileID string) (*messages.IntegrationMetadata, error) { From a5bd8c2ae78a49f039d52509134c4c471b80041f Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 17 Nov 2024 15:05:59 +0100 Subject: [PATCH 5/6] Add API routes to explore integrations content in ui --- internal/ui/handlers.go | 78 +++++++++++++++++++++++++++++++++++++++++ internal/ui/routes.go | 4 +++ 2 files changed, 82 insertions(+) diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index e30e7823..09e7686d 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -6,6 +6,7 @@ import ( "time" "github.com/ddvk/rmfakecloud/internal/common" + "github.com/ddvk/rmfakecloud/internal/integrations" "github.com/ddvk/rmfakecloud/internal/model" "github.com/ddvk/rmfakecloud/internal/storage" "github.com/ddvk/rmfakecloud/internal/ui/viewmodel" @@ -648,3 +649,80 @@ func (app *ReactAppWrapper) deleteIntegration(c *gin.Context) { c.AbortWithStatus(http.StatusNotFound) } + +func (app *ReactAppWrapper) exploreIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + integrationID := common.ParamS(intIDParam, c) + + integrationProvider, err := integrations.GetIntegrationProvider(app.userStorer, uid, integrationID) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + folder := common.ParamS("path", c) + if folder == "" { + folder = "root" + } + + response, err := integrationProvider.List(folder, 2) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, response) +} + +func (app *ReactAppWrapper) getMetadataIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + integrationID := common.ParamS(intIDParam, c) + + integrationProvider, err := integrations.GetIntegrationProvider(app.userStorer, uid, integrationID) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + fileid := common.ParamS("path", c) + + response, err := integrationProvider.GetMetadata(fileid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + c.JSON(http.StatusOK, response) +} + +func (app *ReactAppWrapper) downloadThroughIntegration(c *gin.Context) { + uid := c.GetString(userIDContextKey) + + integrationID := common.ParamS(intIDParam, c) + + integrationProvider, err := integrations.GetIntegrationProvider(app.userStorer, uid, integrationID) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + fileid := common.ParamS("path", c) + + response, size, err := integrationProvider.Download(fileid) + if err != nil { + log.Error(err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + + defer response.Close() + + c.DataFromReader(http.StatusOK, size, "", response, nil) +} diff --git a/internal/ui/routes.go b/internal/ui/routes.go index 4df7fd14..5a6f16fc 100644 --- a/internal/ui/routes.go +++ b/internal/ui/routes.go @@ -79,6 +79,10 @@ func (app *ReactAppWrapper) RegisterRoutes(router *gin.Engine) { auth.PUT("integrations/:intid", app.updateIntegration) auth.DELETE("integrations/:intid", app.deleteIntegration) + auth.GET("integrations/:intid/explore/*path", app.exploreIntegration) + auth.GET("integrations/:intid/metadata/*path", app.getMetadataIntegration) + auth.GET("integrations/:intid/download/*path", app.downloadThroughIntegration) + //admin admin := auth.Group("") admin.Use(app.adminMiddleware()) From f190b663f3a37997529bb903bc230a332f8a1467 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Sun, 17 Nov 2024 15:26:59 +0100 Subject: [PATCH 6/6] integrations: Use int64 for file size --- internal/integrations/integrations.go | 2 +- internal/messages/messages.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/integrations/integrations.go b/internal/integrations/integrations.go index 0dfe29d0..dc172c10 100644 --- a/internal/integrations/integrations.go +++ b/internal/integrations/integrations.go @@ -134,7 +134,7 @@ func visitDir(root, currentPath string, depth int, parentFolder *messages.Integr ID: encodedPath, FileID: encodedPath, Name: docName, - Size: int(d.Size()), + Size: d.Size(), SourceFileType: contentType, } diff --git a/internal/messages/messages.go b/internal/messages/messages.go index 77985ed0..09ef8fa2 100644 --- a/internal/messages/messages.go +++ b/internal/messages/messages.go @@ -200,7 +200,7 @@ type IntegrationFile struct { ID string `json:"id"` Name string `json:"name"` ProvidedFileType string `json:"providedFileType"` - Size int `json:"size"` + Size int64 `json:"size"` SourceFileType string `json:"sourceFileType"` }