diff options
Diffstat (limited to 'workhorse/internal/artifacts/entry.go')
-rw-r--r-- | workhorse/internal/artifacts/entry.go | 123 |
1 files changed, 123 insertions, 0 deletions
diff --git a/workhorse/internal/artifacts/entry.go b/workhorse/internal/artifacts/entry.go new file mode 100644 index 00000000000..0c697d40020 --- /dev/null +++ b/workhorse/internal/artifacts/entry.go @@ -0,0 +1,123 @@ +package artifacts + +import ( + "bufio" + "context" + "fmt" + "io" + "mime" + "net/http" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + + "gitlab.com/gitlab-org/labkit/log" + + "gitlab.com/gitlab-org/gitlab-workhorse/internal/helper" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/senddata" + "gitlab.com/gitlab-org/gitlab-workhorse/internal/zipartifacts" +) + +type entry struct{ senddata.Prefix } +type entryParams struct{ Archive, Entry string } + +var SendEntry = &entry{"artifacts-entry:"} + +// Artifacts downloader doesn't support ranges when downloading a single file +func (e *entry) Inject(w http.ResponseWriter, r *http.Request, sendData string) { + var params entryParams + if err := e.Unpack(¶ms, sendData); err != nil { + helper.Fail500(w, r, fmt.Errorf("SendEntry: unpack sendData: %v", err)) + return + } + + log.WithContextFields(r.Context(), log.Fields{ + "entry": params.Entry, + "archive": params.Archive, + "path": r.URL.Path, + }).Print("SendEntry: sending") + + if params.Archive == "" || params.Entry == "" { + helper.Fail500(w, r, fmt.Errorf("SendEntry: Archive or Entry is empty")) + return + } + + err := unpackFileFromZip(r.Context(), params.Archive, params.Entry, w.Header(), w) + + if os.IsNotExist(err) { + http.NotFound(w, r) + } else if err != nil { + helper.Fail500(w, r, fmt.Errorf("SendEntry: %v", err)) + } +} + +func detectFileContentType(fileName string) string { + contentType := mime.TypeByExtension(filepath.Ext(fileName)) + if contentType == "" { + contentType = "application/octet-stream" + } + return contentType +} + +func unpackFileFromZip(ctx context.Context, archivePath, encodedFilename string, headers http.Header, output io.Writer) error { + fileName, err := zipartifacts.DecodeFileEntry(encodedFilename) + if err != nil { + return err + } + + catFile := exec.Command("gitlab-zip-cat") + catFile.Env = append(os.Environ(), + "ARCHIVE_PATH="+archivePath, + "ENCODED_FILE_NAME="+encodedFilename, + ) + catFile.Stderr = log.ContextLogger(ctx).Writer() + catFile.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} + stdout, err := catFile.StdoutPipe() + if err != nil { + return fmt.Errorf("create gitlab-zip-cat stdout pipe: %v", err) + } + + if err := catFile.Start(); err != nil { + return fmt.Errorf("start %v: %v", catFile.Args, err) + } + defer helper.CleanUpProcessGroup(catFile) + + basename := filepath.Base(fileName) + reader := bufio.NewReader(stdout) + contentLength, err := reader.ReadString('\n') + if err != nil { + if catFileErr := waitCatFile(catFile); catFileErr != nil { + return catFileErr + } + return fmt.Errorf("read content-length: %v", err) + } + contentLength = strings.TrimSuffix(contentLength, "\n") + + // Write http headers about the file + headers.Set("Content-Length", contentLength) + headers.Set("Content-Type", detectFileContentType(fileName)) + headers.Set("Content-Disposition", "attachment; filename=\""+escapeQuotes(basename)+"\"") + // Copy file body to client + if _, err := io.Copy(output, reader); err != nil { + return fmt.Errorf("copy stdout of %v: %v", catFile.Args, err) + } + + return waitCatFile(catFile) +} + +func waitCatFile(cmd *exec.Cmd) error { + err := cmd.Wait() + if err == nil { + return nil + } + + st, ok := helper.ExitStatus(err) + + if ok && (st == zipartifacts.CodeArchiveNotFound || st == zipartifacts.CodeEntryNotFound) { + return os.ErrNotExist + } + return fmt.Errorf("wait for %v to finish: %v", cmd.Args, err) + +} |