From fdba86f9c361b468ede482bcee4bff6e7ee5993e Mon Sep 17 00:00:00 2001 From: frikky Date: Thu, 29 Oct 2020 10:57:05 +0100 Subject: [PATCH] #141: Started file fixing. Added file download handler --- backend/app_sdk/app_base.py | 4 +- backend/app_sdk/build.sh | 2 +- backend/go-app/main.go | 221 ++++++++++++++++++++++++++-- backend/tests/file_download.sh | 3 + frontend/src/components/Header.js | 40 +++-- frontend/src/views/Admin.jsx | 2 +- frontend/src/views/SettingsPage.jsx | 52 +++---- 7 files changed, 272 insertions(+), 52 deletions(-) create mode 100644 backend/tests/file_download.sh diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index a70b44ec..d25297eb 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -400,7 +400,7 @@ class AppBase: # Means it's a single item -> continue if seconditem == "": - print("In first - handling %s", seconditem) + print("In first - handling %s" % seconditem) tmpitem = basejson[int(firstitem)] try: newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:]) @@ -884,7 +884,7 @@ class AppBase: # Custom format for ${name[0,1,2,...]}$ #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" - submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" + submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" actualitem = re.findall(submatch, value, re.MULTILINE) try: if action["skip_multicheck"]: diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 2f622fcd..bfd63f22 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=app_sdk -VERSION=0.7.5 +VERSION=0.7.6 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:$NAME -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 84b9c10b..bd0f2153 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1790,16 +1790,16 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { currentOrg = []byte("{}") } - returnData := fmt.Sprintf(` - { - "success": true, - "admin": %s, - "tutorials": [], - "id": "%s", - "orgs": [%s], - "active_org": %s, - "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}] - }`, parsedAdmin, userInfo.Id, currentOrg, currentOrg, userInfo.Session, expiration.Unix()) + returnData := fmt.Sprintf(`{ + "success": true, + "username": "%s", + "admin": %s, + "tutorials": [], + "id": "%s", + "orgs": [%s], + "active_org": %s, + "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}] +}`, userInfo.Username, parsedAdmin, userInfo.Id, currentOrg, currentOrg, userInfo.Session, expiration.Unix()) resp.WriteHeader(200) resp.Write([]byte(returnData)) @@ -2537,6 +2537,28 @@ func getSession(ctx context.Context, thissession string) (*session, error) { return curUser, nil } +// ListBooks returns a list of books, ordered by title. +func getFile(ctx context.Context, id string) (*File, error) { + key := datastore.NameKey("Files", id, nil) + curFile := &File{} + if err := dbclient.Get(ctx, key, curFile); err != nil { + return &File{}, err + } + + return curFile, nil +} + +func setFile(ctx context.Context, file File) error { + // clear session_token and API_token for user + k := datastore.NameKey("Files", file.Id, nil) + if _, err := dbclient.Put(ctx, k, &file); err != nil { + log.Println(err) + return err + } + + return nil +} + // ListBooks returns a list of books, ordered by title. func getOrg(ctx context.Context, id string) (*Org, error) { key := datastore.NameKey("Organizations", id, nil) @@ -6648,6 +6670,38 @@ func runInit(ctx context.Context) { log.Printf("Set workflow orgs for %d workflows", updated) } } + + fileq := datastore.NewQuery("Files").Limit(1) + count, err := dbclient.Count(ctx, fileq) + + if err == nil && count == 0 { + basepath := "." + filename := "testfile.txt" + fileId := uuid.NewV4().String() + log.Printf("Creating new file reference %s because none exist!", fileId) + workflowId := "2e9d6474-402c-4dcc-bb53-45f638ca18d3" + downloadPath := fmt.Sprintf("%s/%s/%s/%s", basepath, activeOrgs[0].Id, workflowId, fileId) + + timeNow := time.Now().Unix() + newFile := File{ + Id: fileId, + CreatedAt: timeNow, + UpdatedAt: timeNow, + Description: "Created by system for testing", + Status: "active", + Filename: filename, + OrgId: activeOrgs[0].Id, + WorkflowId: workflowId, + DownloadPath: downloadPath, + } + + err = setFile(ctx, newFile) + if err != nil { + log.Printf("Failed setting file: %s", err) + } else { + log.Printf("Created file %s in init", newFile.DownloadPath) + } + } } // Gets schedules and starts them @@ -7183,6 +7237,141 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { resp.Write(respBody) } +type File struct { + Id string `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + CreatedAt int64 `json:"created_at" datastore:"created_at"` + UpdatedAt int64 `json:"updated_at" datastore:"updated_at"` + CreatedBy struct { + ID int `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + Login string `json:"login" datastore:"login"` + Name string `json:"name" datastore:"name"` + } `json:"created_by" datastore:"created_by"` + Description string `json:"description" datastore:"description"` + Etag int `json:"etag" datastore:"etag"` + ExpiresAt string `json:"expires_at" datastore:"expires_at"` + Folder struct { + ID int `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + Etag int `json:"etag" datastore:"etag"` + Name string `json:"name" datastore:"name"` + SequenceID int `json:"sequence_id" datastore:"sequence_id"` + } `json:"folder" datastore:"folder"` + Status string `json:"status" datastore:"status"` + Filename string `json:"filename" datastore:"filename"` + UpdatedBy struct { + ID int `json:"id" datastore:"id"` + Type string `json:"type" datastore:"type"` + Login string `json:"login" datastore:"login"` + Name string `json:"name" datastore:"name"` + } `json:"updated_by" datastore:"updated_by"` + URL string `json:"url" datastore:"org"` + OrgId string `json:"org_id" datastore:"org_id"` + WorkflowId string `json:"workflow_id" datastore:"workflow_id"` + Workflows []string `json:"workflows" datastore:"workflows"` + DownloadPath string `json:"download_path" datastore:"download_path"` +} + +func handleGetFileContent(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + var fileId string + location := strings.Split(request.URL.String(), "/") + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("Path too short: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + fileId = location[4] + } + + //log.Printf("In file download") + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in file download: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // 1. Verify if the user has access to the file: org_id and workflow + + log.Printf("Should get file %s", fileId) + ctx := context.Background() + file, err := getFile(ctx, fileId) + if err != nil { + log.Printf("File %s not found: %s", fileId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + found := false + if file.OrgId == user.ActiveOrg.Id { + found = true + } else { + for _, item := range user.Orgs { + if item == file.OrgId { + found = true + break + } + } + } + + if !found { + log.Printf("User %s doesn't have access to %s", user.Username, fileId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Fixme: More auth: org and workflow! + downloadPath := file.DownloadPath + log.Printf("Downloadpath: %s", downloadPath) + Openfile, err := os.Open(downloadPath) + defer Openfile.Close() //Close after function return + if err != nil { + //File not found, send 404 + http.Error(resp, "File not found.", 404) + return + } + + //File is found, create and send the correct headers + //Get the Content-Type of the file + //Create a buffer to store the header of the file in + FileHeader := make([]byte, 512) + //Copy the headers into the FileHeader buffer + Openfile.Read(FileHeader) + //Get content type of file + FileContentType := http.DetectContentType(FileHeader) + + //Get the file size + FileStat, _ := Openfile.Stat() //Get info from file + FileSize := strconv.FormatInt(FileStat.Size(), 10) //Get file size as a string + + //Send the headers + resp.Header().Set("Content-Disposition", "attachment; filename="+fileId) + resp.Header().Set("Content-Type", FileContentType) + resp.Header().Set("Content-Length", FileSize) + + //Send the file + //We read 512 bytes from the file already, so we reset the offset back to 0 + Openfile.Seek(0, 0) + io.Copy(resp, Openfile) //'Copy' the file to the client + return + + //log.Printf("Should download file %s", downloadPath) + //resp.WriteHeader(200) + //resp.Write([]byte("OK")) +} + func initHandlers() { var err error ctx := context.Background() @@ -7302,7 +7491,17 @@ func initHandlers() { // NEW for 0.8.0 r.HandleFunc("/api/v1/cloud/setup", handleCloudSetup).Methods("POST", "OPTIONS") - r.HandleFunc("/api/v1/getorgs", handleGetOrgs).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/orgs", handleGetOrgs).Methods("GET", "OPTIONS") + + // Important for email, IDS etc. Create this by: + // PS: For cloud, this has to use cloud storage. + // https://developer.box.com/reference/get-files-id-content/ + // 1. Creating the "get file" option. Make it possible to run this in the frontend. + r.HandleFunc("/api/v1/files/{fileId}/content", handleGetFileContent).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/files/create", handleGetFile).Methods("POST", "OPTIONS") + //r.HandleFunc("/api/v1/files/upload", handleGetFile).Methods("POST", "OPTIONS") + //r.HandleFunc("/api/v1/files/{fileId}", handleGetFile).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/files/{fileId}", handleGetFile).Methods("DELETE", "OPTIONS") http.Handle("/", r) } diff --git a/backend/tests/file_download.sh b/backend/tests/file_download.sh new file mode 100644 index 00000000..32bb0787 --- /dev/null +++ b/backend/tests/file_download.sh @@ -0,0 +1,3 @@ +# ./b199646b-16d2-456d-9fd6-b9972e929466/2e9d6474-402c-4dcc-bb53-45f638ca18d3/0d676d72-5d53-4803-a6b0-4afb464df828 +# org_id / workflow_id / file_id +curl http://192.168.3.6:5001/api/v1/files/0d676d72-5d53-4803-a6b0-4afb464df828/content -H "Authorization: Bearer 093b576f-19ea-4353-b685-362ab50f39f4" diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index 6902eb8f..4bc78430 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -35,7 +35,7 @@ const Header = props => { // DEBUG HERE const handleClickLogout = () => { - console.log("SHOULD LOG OUT") + console.log("SHOULD LOG OUT") console.log(isLoggedIn) // Don't really care about the logout @@ -47,10 +47,9 @@ const Header = props => { }, }) .then(() => { - // Log out anyway - console.log("Hey") - removeCookie("session_token", {path: "/"}) - window.location.pathname = "/" + // Log out anyway + removeCookie("session_token", {path: "/"}) + //window.location.pathname = "/" }) .catch(error => { console.log(error) @@ -158,6 +157,16 @@ const Header = props => { + {/* + + +
+ + Pricing +
+ +
+ */} {/* @@ -183,6 +192,19 @@ const Header = props => { color="primary"> Settings + {/* + + + + + + */} {userdata === undefined || userdata.admin === undefined || userdata.admin === null || !userdata.admin ? null : @@ -299,8 +321,8 @@ const Header = props => { // - const loadedCheck = isLoaded ? -
+ const loadedCheck = +
{loginTextBrowser} @@ -308,10 +330,6 @@ const Header = props => { {loginTextMobile}
- : -
-
- //
return (
diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 298080f8..9978dd97 100644 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -555,7 +555,7 @@ const Admin = (props) => { } const getOrgs = () => { - fetch(globalUrl + "/api/v1/getorgs", { + fetch(globalUrl + "/api/v1/orgs", { method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/frontend/src/views/SettingsPage.jsx b/frontend/src/views/SettingsPage.jsx index d91d74fd..0f7c572a 100644 --- a/frontend/src/views/SettingsPage.jsx +++ b/frontend/src/views/SettingsPage.jsx @@ -109,13 +109,13 @@ const Settings = (props) => { const getSettings = () => { fetch(globalUrl+"/api/v1/getsettings", { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - credentials: "include", - }) + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for WORKFLOW EXECUTION :O!") @@ -137,24 +137,24 @@ const Settings = (props) => { if (userInfo.username.length > 0) { setUsername(userInfo.username) } - if (userInfo.firstname.length > 0) { - setFirstname(userInfo.firstname) - } - if (userInfo.lastname.length > 0) { - setLastname(userInfo.lastname) - } - if (userInfo.title.length > 0) { - setTitle(userInfo.title) - } - if (userInfo.companyname.length > 0) { - setCompanyname(userInfo.companyname) - } - if (userInfo.phone.length > 0) { - setPhone(userInfo.phone) - } - if (userInfo.email.length > 0) { - setEmail(userInfo.email) - } + //if (userInfo.firstname.length > 0) { + // setFirstname(userInfo.firstname) + //} + //if (userInfo.lastname.length > 0) { + // setLastname(userInfo.lastname) + //} + //if (userInfo.title.length > 0) { + // setTitle(userInfo.title) + //} + //if (userInfo.companyname.length > 0) { + // setCompanyname(userInfo.companyname) + //} + //if (userInfo.phone.length > 0) { + // setPhone(userInfo.phone) + //} + //if (userInfo.email.length > 0) { + // setEmail(userInfo.email) + //} } } @@ -175,7 +175,7 @@ const Settings = (props) => {

APIKEY

- What is the API key used for? + What is the API key used for?