File: entry.go

package info (click to toggle)
gitlab 17.6.5-19
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 629,368 kB
  • sloc: ruby: 1,915,304; javascript: 557,307; sql: 60,639; xml: 6,509; sh: 4,567; makefile: 1,239; python: 406
file content (136 lines) | stat: -rw-r--r-- 3,902 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// Package artifacts provides functionality for managing artifacts.
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/labkit/mask"

	"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/command"
	"gitlab.com/gitlab-org/gitlab/workhorse/internal/helper/fail"
	"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 }

// SendEntry is a predefined entry used for sending artifacts.
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(&params, sendData); err != nil {
		fail.Request(w, r, fmt.Errorf("SendEntry: unpack sendData: %v", err))
		return
	}

	log.WithContextFields(r.Context(), log.Fields{
		"entry":   params.Entry,
		"archive": mask.URL(params.Archive),
		"path":    r.URL.Path,
	}).Print("SendEntry: sending")

	if params.Archive == "" || params.Entry == "" {
		fail.Request(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 {
		fail.Request(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
	}

	logWriter := log.ContextLogger(ctx).Writer()
	defer func() {
		if closeErr := logWriter.Close(); closeErr != nil {
			log.ContextLogger(ctx).WithError(closeErr).Error("failed to close gitlab-zip-cat log writer")
		}
	}()

	catFile := exec.Command("gitlab-zip-cat")
	catFile.Env = append(os.Environ(),
		"ARCHIVE_PATH="+archivePath,
		"ENCODED_FILE_NAME="+encodedFilename,
	)
	catFile.Stderr = logWriter
	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 func() {
		if err = command.KillProcessGroup(catFile); err != nil {
			fmt.Printf("failed to kill process group: %v\n", err)
		}
	}()

	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 := command.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)
}