github.com/nektos/act@v0.2.63-0.20240520024548-8acde99bfa9c/pkg/artifacts/server.go (about) 1 package artifacts 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "io/fs" 10 "net/http" 11 "os" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/julienschmidt/httprouter" 17 18 "github.com/nektos/act/pkg/common" 19 ) 20 21 type FileContainerResourceURL struct { 22 FileContainerResourceURL string `json:"fileContainerResourceUrl"` 23 } 24 25 type NamedFileContainerResourceURL struct { 26 Name string `json:"name"` 27 FileContainerResourceURL string `json:"fileContainerResourceUrl"` 28 } 29 30 type NamedFileContainerResourceURLResponse struct { 31 Count int `json:"count"` 32 Value []NamedFileContainerResourceURL `json:"value"` 33 } 34 35 type ContainerItem struct { 36 Path string `json:"path"` 37 ItemType string `json:"itemType"` 38 ContentLocation string `json:"contentLocation"` 39 } 40 41 type ContainerItemResponse struct { 42 Value []ContainerItem `json:"value"` 43 } 44 45 type ResponseMessage struct { 46 Message string `json:"message"` 47 } 48 49 type WritableFile interface { 50 io.WriteCloser 51 } 52 53 type WriteFS interface { 54 OpenWritable(name string) (WritableFile, error) 55 OpenAppendable(name string) (WritableFile, error) 56 } 57 58 type readWriteFSImpl struct { 59 } 60 61 func (fwfs readWriteFSImpl) Open(name string) (fs.File, error) { 62 return os.Open(name) 63 } 64 65 func (fwfs readWriteFSImpl) OpenWritable(name string) (WritableFile, error) { 66 if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil { 67 return nil, err 68 } 69 return os.OpenFile(name, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0o644) 70 } 71 72 func (fwfs readWriteFSImpl) OpenAppendable(name string) (WritableFile, error) { 73 if err := os.MkdirAll(filepath.Dir(name), os.ModePerm); err != nil { 74 return nil, err 75 } 76 file, err := os.OpenFile(name, os.O_CREATE|os.O_RDWR, 0o644) 77 78 if err != nil { 79 return nil, err 80 } 81 82 _, err = file.Seek(0, io.SeekEnd) 83 if err != nil { 84 return nil, err 85 } 86 return file, nil 87 } 88 89 var gzipExtension = ".gz__" 90 91 func safeResolve(baseDir string, relPath string) string { 92 return filepath.Join(baseDir, filepath.Clean(filepath.Join(string(os.PathSeparator), relPath))) 93 } 94 95 func uploads(router *httprouter.Router, baseDir string, fsys WriteFS) { 96 router.POST("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 97 runID := params.ByName("runId") 98 99 json, err := json.Marshal(FileContainerResourceURL{ 100 FileContainerResourceURL: fmt.Sprintf("http://%s/upload/%s", req.Host, runID), 101 }) 102 if err != nil { 103 panic(err) 104 } 105 106 _, err = w.Write(json) 107 if err != nil { 108 panic(err) 109 } 110 }) 111 112 router.PUT("/upload/:runId", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 113 itemPath := req.URL.Query().Get("itemPath") 114 runID := params.ByName("runId") 115 116 if req.Header.Get("Content-Encoding") == "gzip" { 117 itemPath += gzipExtension 118 } 119 120 safeRunPath := safeResolve(baseDir, runID) 121 safePath := safeResolve(safeRunPath, itemPath) 122 123 file, err := func() (WritableFile, error) { 124 contentRange := req.Header.Get("Content-Range") 125 if contentRange != "" && !strings.HasPrefix(contentRange, "bytes 0-") { 126 return fsys.OpenAppendable(safePath) 127 } 128 return fsys.OpenWritable(safePath) 129 }() 130 131 if err != nil { 132 panic(err) 133 } 134 defer file.Close() 135 136 writer, ok := file.(io.Writer) 137 if !ok { 138 panic(errors.New("File is not writable")) 139 } 140 141 if req.Body == nil { 142 panic(errors.New("No body given")) 143 } 144 145 _, err = io.Copy(writer, req.Body) 146 if err != nil { 147 panic(err) 148 } 149 150 json, err := json.Marshal(ResponseMessage{ 151 Message: "success", 152 }) 153 if err != nil { 154 panic(err) 155 } 156 157 _, err = w.Write(json) 158 if err != nil { 159 panic(err) 160 } 161 }) 162 163 router.PATCH("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 164 json, err := json.Marshal(ResponseMessage{ 165 Message: "success", 166 }) 167 if err != nil { 168 panic(err) 169 } 170 171 _, err = w.Write(json) 172 if err != nil { 173 panic(err) 174 } 175 }) 176 } 177 178 func downloads(router *httprouter.Router, baseDir string, fsys fs.FS) { 179 router.GET("/_apis/pipelines/workflows/:runId/artifacts", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 180 runID := params.ByName("runId") 181 182 safePath := safeResolve(baseDir, runID) 183 184 entries, err := fs.ReadDir(fsys, safePath) 185 if err != nil { 186 panic(err) 187 } 188 189 var list []NamedFileContainerResourceURL 190 for _, entry := range entries { 191 list = append(list, NamedFileContainerResourceURL{ 192 Name: entry.Name(), 193 FileContainerResourceURL: fmt.Sprintf("http://%s/download/%s", req.Host, runID), 194 }) 195 } 196 197 json, err := json.Marshal(NamedFileContainerResourceURLResponse{ 198 Count: len(list), 199 Value: list, 200 }) 201 if err != nil { 202 panic(err) 203 } 204 205 _, err = w.Write(json) 206 if err != nil { 207 panic(err) 208 } 209 }) 210 211 router.GET("/download/:container", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 212 container := params.ByName("container") 213 itemPath := req.URL.Query().Get("itemPath") 214 safePath := safeResolve(baseDir, filepath.Join(container, itemPath)) 215 216 var files []ContainerItem 217 err := fs.WalkDir(fsys, safePath, func(path string, entry fs.DirEntry, err error) error { 218 if !entry.IsDir() { 219 rel, err := filepath.Rel(safePath, path) 220 if err != nil { 221 panic(err) 222 } 223 224 // if it was upload as gzip 225 rel = strings.TrimSuffix(rel, gzipExtension) 226 path := filepath.Join(itemPath, rel) 227 228 rel = filepath.ToSlash(rel) 229 path = filepath.ToSlash(path) 230 231 files = append(files, ContainerItem{ 232 Path: path, 233 ItemType: "file", 234 ContentLocation: fmt.Sprintf("http://%s/artifact/%s/%s/%s", req.Host, container, itemPath, rel), 235 }) 236 } 237 return nil 238 }) 239 if err != nil { 240 panic(err) 241 } 242 243 json, err := json.Marshal(ContainerItemResponse{ 244 Value: files, 245 }) 246 if err != nil { 247 panic(err) 248 } 249 250 _, err = w.Write(json) 251 if err != nil { 252 panic(err) 253 } 254 }) 255 256 router.GET("/artifact/*path", func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { 257 path := params.ByName("path")[1:] 258 259 safePath := safeResolve(baseDir, path) 260 261 file, err := fsys.Open(safePath) 262 if err != nil { 263 // try gzip file 264 file, err = fsys.Open(safePath + gzipExtension) 265 if err != nil { 266 panic(err) 267 } 268 w.Header().Add("Content-Encoding", "gzip") 269 } 270 271 _, err = io.Copy(w, file) 272 if err != nil { 273 panic(err) 274 } 275 }) 276 } 277 278 func Serve(ctx context.Context, artifactPath string, addr string, port string) context.CancelFunc { 279 serverContext, cancel := context.WithCancel(ctx) 280 logger := common.Logger(serverContext) 281 282 if artifactPath == "" { 283 return cancel 284 } 285 286 router := httprouter.New() 287 288 logger.Debugf("Artifacts base path '%s'", artifactPath) 289 fsys := readWriteFSImpl{} 290 uploads(router, artifactPath, fsys) 291 downloads(router, artifactPath, fsys) 292 293 server := &http.Server{ 294 Addr: fmt.Sprintf("%s:%s", addr, port), 295 ReadHeaderTimeout: 2 * time.Second, 296 Handler: router, 297 } 298 299 // run server 300 go func() { 301 logger.Infof("Start server on http://%s:%s", addr, port) 302 if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 303 logger.Fatal(err) 304 } 305 }() 306 307 // wait for cancel to gracefully shutdown server 308 go func() { 309 <-serverContext.Done() 310 311 if err := server.Shutdown(ctx); err != nil { 312 logger.Errorf("Failed shutdown gracefully - force shutdown: %v", err) 313 server.Close() 314 } 315 }() 316 317 return cancel 318 }