-
Notifications
You must be signed in to change notification settings - Fork 11
[API] Add "import/export app" commands #181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
922ab70
implement export app
mirkoCrobu 4d1d6c3
add test e2e export
mirkoCrobu c78331f
add import endpoint
mirkoCrobu d636791
add test e2e for import endpoint
mirkoCrobu aa7d42a
refactoring and finalize implementation
mirkoCrobu 96ec574
fix export app
mirkoCrobu 8d33b8a
fix export docs and e2e test
mirkoCrobu dc4bbf5
refactorign import
mirkoCrobu c67dc74
fix import
mirkoCrobu 86d675a
implement copilot code review
mirkoCrobu faf78a0
fix e2e tests
mirkoCrobu 8c5060b
implement atomic swap for import app
mirkoCrobu 9b3911f
implement security checks
mirkoCrobu d297c1c
update go mod
mirkoCrobu 8653a4a
rename function
mirkoCrobu db1a60e
increare export performance
mirkoCrobu fb8ebe6
refactoring handler
mirkoCrobu f087ca4
add unit tests
mirkoCrobu 7a10bd2
update doc openapi and e2e test for import endpoint
mirkoCrobu 1852e2c
fix format bae64 to binary in openapi generated file
mirkoCrobu 488e861
improve import with new specification
mirkoCrobu e758c09
code review fixes
mirkoCrobu 70ad2dc
add include_data param for export_app endpoint
mirkoCrobu 10f8cc2
fix test export api
mirkoCrobu e03b5c1
code review fix
mirkoCrobu 61af008
use path pkg func to create tmp dir
mirkoCrobu 9037f3c
using rand text instead of uuid
mirkoCrobu c548bd3
add filter to avoid listing hidden tmp apps
mirkoCrobu 485a828
update dependencies
mirkoCrobu 6e4baf3
revew fixes
mirkoCrobu 70a6827
add comment
mirkoCrobu 9b9a0d1
move ZipAppToBuffer to app pkg
mirkoCrobu 07d1b17
move ReadAppDescriptorFromZip to app pkg
mirkoCrobu f51d9ad
move ValidateAppZipContent to app pkg
mirkoCrobu a1e8ae9
ignore tmp apps into list app and tests
mirkoCrobu f3203c5
fix error message
mirkoCrobu fcdb785
fome exporto to app package
mirkoCrobu 504f197
move import/export to orchestrator pkg
mirkoCrobu 40b05bb
fix app structure validation
mirkoCrobu e96234b
set better error return value
mirkoCrobu ad7cf12
set better error return value
mirkoCrobu d30aec0
make lint happy
mirkoCrobu f88de34
fix error messages
mirkoCrobu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package handlers | ||
|
mirkoCrobu marked this conversation as resolved.
|
||
|
|
||
| import ( | ||
| "errors" | ||
| "log/slog" | ||
| "net/http" | ||
| "os" | ||
|
|
||
| "github.com/arduino/arduino-app-cli/internal/api/models" | ||
| "github.com/arduino/arduino-app-cli/internal/orchestrator" | ||
| "github.com/arduino/arduino-app-cli/internal/orchestrator/app" | ||
| "github.com/arduino/arduino-app-cli/internal/orchestrator/config" | ||
| "github.com/arduino/arduino-app-cli/internal/render" | ||
| ) | ||
|
|
||
| func HandleAppExport( | ||
| idProvider *app.IDProvider, | ||
| cfg config.Configuration, | ||
| ) http.HandlerFunc { | ||
| return func(w http.ResponseWriter, r *http.Request) { | ||
| id, err := idProvider.IDFromBase64(r.PathValue("appID")) | ||
| if err != nil { | ||
| render.EncodeResponse(w, http.StatusPreconditionFailed, models.ErrorResponse{Details: "invalid id"}) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| return | ||
| } | ||
| app, err := app.Load(id.ToPath()) | ||
| if err != nil { | ||
| slog.Error("Unable to parse the app.yaml", slog.String("error", err.Error()), slog.String("path", id.String())) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| if errors.Is(err, os.ErrNotExist) { | ||
| render.EncodeResponse(w, http.StatusNotFound, models.ErrorResponse{Details: "unable to find the app"}) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "unable to parse the app"}) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| } | ||
| return | ||
| } | ||
|
|
||
| zipBytes, fileName, err := orchestrator.ExportAppZip(r.Context(), app) | ||
| if err != nil { | ||
| slog.Error("failed to export app", slog.String("app_id", id.String()), slog.String("error", err.Error())) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{ | ||
| Details: "Failed to generate archive.", | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| }) | ||
| return | ||
| } | ||
|
|
||
| render.EncodeZipResponse(w, http.StatusOK, zipBytes, fileName) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| // This file is part of arduino-app-cli. | ||
| // | ||
| // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) | ||
| // ... (License header standard) ... | ||
|
|
||
| package handlers | ||
|
|
||
| import ( | ||
| "errors" | ||
| "io" | ||
| "log/slog" | ||
| "net/http" | ||
| "os" | ||
| "strings" | ||
|
|
||
| "github.com/arduino/arduino-app-cli/internal/api/models" | ||
| "github.com/arduino/arduino-app-cli/internal/orchestrator" | ||
| "github.com/arduino/arduino-app-cli/internal/orchestrator/app" | ||
| "github.com/arduino/arduino-app-cli/internal/orchestrator/config" | ||
| "github.com/arduino/arduino-app-cli/internal/render" | ||
| ) | ||
|
|
||
| type ImportResponse struct { | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| ID string `json:"id"` | ||
| } | ||
|
|
||
| func HandleAppImport( | ||
| cfg config.Configuration, | ||
| idProvider *app.IDProvider, | ||
| ) http.HandlerFunc { | ||
| return func(w http.ResponseWriter, r *http.Request) { | ||
| folderName := strings.TrimSpace(r.FormValue("folder_name")) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
mirkoCrobu marked this conversation as resolved.
Outdated
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| if folderName == "" { | ||
| render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "missing required 'folder_name' parameter"}) | ||
| return | ||
| } | ||
|
|
||
| file, _, err := r.FormFile("file") | ||
| if err != nil { | ||
| slog.Error("missing file parameter", slog.String("error", err.Error())) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: "missing required file parameter"}) | ||
| return | ||
| } | ||
| defer file.Close() | ||
|
|
||
| tempFile, err := os.CreateTemp("", "app-import-*.zip") | ||
| if err != nil { | ||
| slog.Error("unable to create temp file", slog.String("error", err.Error())) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "internal server error"}) | ||
| return | ||
| } | ||
| tempPath := tempFile.Name() | ||
| defer os.Remove(tempPath) | ||
|
|
||
| if _, err := io.Copy(tempFile, file); err != nil { | ||
| tempFile.Close() | ||
| slog.Error("unable to save upload to temp file", slog.String("error", err.Error())) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "failed to save uploaded file"}) | ||
| return | ||
| } | ||
| tempFile.Close() | ||
|
|
||
| appID, err := orchestrator.ImportAppFromZip(cfg, tempPath, folderName, idProvider) | ||
| if err != nil { | ||
| handleImportError(w, err) | ||
| return | ||
| } | ||
|
|
||
| slog.Info("app imported successfully", slog.String("app_id", appID)) | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
| render.EncodeResponse(w, http.StatusCreated, ImportResponse{ID: appID}) | ||
| } | ||
| } | ||
|
|
||
| func handleImportError(w http.ResponseWriter, err error) { | ||
| slog.Error("import failed", slog.String("error", err.Error())) | ||
|
|
||
| if errors.Is(err, orchestrator.ErrAppAlreadyExists) { | ||
| render.EncodeResponse(w, http.StatusConflict, models.ErrorResponse{Details: err.Error()}) | ||
| return | ||
| } | ||
| if errors.Is(err, orchestrator.ErrBadRequest) || | ||
| strings.Contains(err.Error(), "not a valid zip file") { | ||
| render.EncodeResponse(w, http.StatusBadRequest, models.ErrorResponse{Details: err.Error()}) | ||
| return | ||
| } | ||
|
|
||
| render.EncodeResponse(w, http.StatusInternalServerError, models.ErrorResponse{Details: "failed to process the archive: " + err.Error()}) | ||
| } | ||
|
mirkoCrobu marked this conversation as resolved.
Outdated
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.