Permalink
Cannot retrieve contributors at this time
314 lines (255 sloc)
7.18 KB
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
act/pkg/artifacts/server.go /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This file contains 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
| package artifacts | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "errors" | |
| "fmt" | |
| "io" | |
| "io/fs" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "strings" | |
| "time" | |
| "github.com/julienschmidt/httprouter" | |
| "github.com/nektos/act/pkg/common" | |
| ) | |
| type FileContainerResourceURL struct { | |
| FileContainerResourceURL string `json:"fileContainerResourceUrl"` | |
| } | |
| type NamedFileContainerResourceURL struct { | |
| Name string `json:"name"` | |
| FileContainerResourceURL string `json:"fileContainerResourceUrl"` | |
| } | |
| type NamedFileContainerResourceURLResponse struct { | |
| Count int `json:"count"` | |
| Value []NamedFileContainerResourceURL `json:"value"` | |
| } | |
| type ContainerItem struct { | |
| Path string `json:"path"` | |
| ItemType string `json:"itemType"` | |
| ContentLocation string `json:"contentLocation"` | |
| } | |
| type ContainerItemResponse struct { | |
| Value []ContainerItem `json:"value"` | |
| } | |
| type ResponseMessage struct { | |
| Message string `json:"message"` | |
| } | |
| type WritableFile interface { | |
| io.WriteCloser | |
| } | |
| type WriteFS interface { | |
| OpenWritable(name string) (WritableFile, error) | |
| OpenAppendable(name string) (WritableFile, error) | |
| } | |
| type readWriteFSImpl struct { | |
| } | |
| func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) { | |
| return os.Open(name) | |
| } | |
| func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) { | |
| if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil { | |
| return nil, err | |
| } | |
| return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) | |
| } | |
| func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) { | |
| if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil { | |
| return nil, err | |
| } | |
| file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644) | |
| if err != nil { | |
| return nil, err | |
| } | |
| _, err = file.Seek(0, os.SEEK_END) | |
| if err != nil { | |
| return nil, err | |
| } | |
| return file, nil | |
| } | |
| var gzipExtension = ".gz__" | |
| func safeResolve(baseDir string, relPath string) string { | |
| return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath))) | |
| } | |
| func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) { | |
| router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { | |
| runID := params.ByName("runId") | |
| json, err := json.Marshal(FileContainerResourceURL{ | |
| FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID), | |
| }) | |
| if err != nil { | |
| panic(err) | |
| } | |
| _, err = w.Write(json) | |
| if err != nil { | |
| panic(err) | |
| } | |
| }) | |
| router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { | |
| itemPath := req.URL.Query().Get("itemPath") | |
| runID := params.ByName("runId") | |
| if req.Header.Get("Content-Encoding") == "gzip" { | |
| itemPath += gzipExtension | |
| } | |
| safeRunPath := safeResolve(baseDir, runID) | |
| safePath := safeResolve(safeRunPath, itemPath) | |
| file, err := func() (WritableFile, error) { | |
| contentRange := req.Header.Get("Content-Range") | |
| if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") { | |
| return fsys.OpenAppendable(safePath) | |
| } | |
| return fsys.OpenWritable(safePath) | |
| }() | |
| if err != nil { | |
| panic(err) | |
| } | |
| defer file.Close() | |
| writer, ok := file.(io.Writer) | |
| if !ok { | |
| panic(errors.New("File is not writable")) | |
| } | |
| if req.Body == nil { | |
| panic(errors.New("No body given")) | |
| } | |
| _, err = io.Copy(writer, req.Body) | |
| if err != nil { | |
| panic(err) | |
| } | |
| json, err := json.Marshal(ResponseMessage{ | |
| Message: "success", | |
| }) | |
| if err != nil { | |
| panic(err) | |
| } | |
| _, err = w.Write(json) | |
| if err != nil { | |
| panic(err) | |
| } | |
| }) | |
| router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { | |
| json, err := json.Marshal(ResponseMessage{ | |
| Message: "success", | |
| }) | |
| if err != nil { | |
| panic(err) | |
| } | |
| _, err = w.Write(json) | |
| if err != nil { | |
| panic(err) | |
| } | |
| }) | |
| } | |
| func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) { | |
| router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { | |
| runID := params.ByName("runId") | |
| safePath := safeResolve(baseDir, runID) | |
| entries, err := fs.ReadDir(fsys, safePath) | |
| if err != nil { | |
| panic(err) | |
| } | |
| var list []NamedFileContainerResourceURL | |
| for _, entry := range entries { | |
| list = append(list, NamedFileContainerResourceURL{ | |
| Name: entry.Name(), | |
| FileContainerResourceURL: fmt.Sprintf("http://%s/download/%s", req.Host, runID), | |
| }) | |
| } | |
| json, err := json.Marshal(NamedFileContainerResourceURLResponse{ | |
| Count: len(list), | |
| Value: list, | |
| }) | |
| if err != nil { | |
| panic(err) | |
| } | |
| _, err = w.Write(json) | |
| if err != nil { | |
| panic(err) | |
| } | |
| }) | |
| router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { | |
| container := params.ByName("container") | |
| itemPath := req.URL.Query().Get("itemPath") | |
| safePath := safeResolve(baseDir, filepath.Join(container, itemPath)) | |
| var files []ContainerItem | |
| err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error { | |
| if !entry.IsDir() { | |
| rel, err := filepath.Rel(safePath, path) | |
| if err != nil { | |
| panic(err) | |
| } | |
| // if it was upload as gzip | |
| rel = strings.TrimSuffix(rel, gzipExtension) | |
| files = append(files, ContainerItem{ | |
| Path: filepath.Join(itemPath, rel), | |
| ItemType: "file", | |
| ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel), | |
| }) | |
| } | |
| return nil | |
| }) | |
| if err != nil { | |
| panic(err) | |
| } | |
| json, err := json.Marshal(ContainerItemResponse{ | |
| Value: files, | |
| }) | |
| if err != nil { | |
| panic(err) | |
| } | |
| _, err = w.Write(json) | |
| if err != nil { | |
| panic(err) | |
| } | |
| }) | |
| router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { | |
| path := params.ByName("path")[1:] | |
| safePath := safeResolve(baseDir, path) | |
| file, err := fsys.Open(safePath) | |
| if err != nil { | |
| // try gzip file | |
| file, err = fsys.Open(safePath + gzipExtension) | |
| if err != nil { | |
| panic(err) | |
| } | |
| w.Header().Add("Content-Encoding", "gzip") | |
| } | |
| _, err = io.Copy(w, file) | |
| if err != nil { | |
| panic(err) | |
| } | |
| }) | |
| } | |
| func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc { | |
| serverContext, cancel := context.WithCancel(ctx) | |
| logger := common.Logger(serverContext) | |
| if artifactPath == "" { | |
| return cancel | |
| } | |
| router := httprouter.New() | |
| logger.Debugf("Artifacts base path '%s'", artifactPath) | |
| fsys := readWriteFSImpl{} | |
| uploads(router, artifactPath, fsys) | |
| downloads(router, artifactPath, fsys) | |
| server := &http.Server{ | |
| Addr: fmt.Sprintf("%s:%s", addr, port), | |
| ReadHeaderTimeout: 2 * time.Second, | |
| Handler: router, | |
| } | |
| // run server | |
| go func() { | |
| logger.Infof("Start server on http://%s:%s", addr, port) | |
| if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { | |
| logger.Fatal(err) | |
| } | |
| }() | |
| // wait for cancel to gracefully shutdown server | |
| go func() { | |
| <-serverContext.Done() | |
| if err := server.Shutdown(ctx); err != nil { | |
| logger.Errorf("Failed shutdown gracefully - force shutdown: %v", err) | |
| server.Close() | |
| } | |
| }() | |
| return cancel | |
| } |