flamenco/pkg/shaman/checkout/definition.go
Sybren A. Stüvel 4e8e71e4e2 Initial checkin of Shaman of Flamenco 2
This is not yet working, it's just a direct copy of the Manager of Flamenco
2, with Logrus replaced by Zerolog. The API has been documented in
flamenco-manager.yaml as a starting point for the integration.
2022-03-25 14:10:26 +01:00

169 lines
4.6 KiB
Go

/* (c) 2019, Blender Foundation - Sybren A. Stüvel
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package checkout
import (
"bufio"
"context"
"fmt"
"io"
"path"
"regexp"
"strconv"
"strings"
"github.com/sirupsen/logrus"
)
/* Checkout Definition files contain a line for each to-be-checked-out file.
* Each line consists of three fields: checksum, file size, path in the checkout.
*/
// FileInvalidError is returned when there is an invalid line in a checkout definition file.
type FileInvalidError struct {
lineNumber int // base-1 line number that's bad
innerErr error
reason string
}
func (cfie FileInvalidError) Error() string {
return fmt.Sprintf("invalid line %d: %s", cfie.lineNumber, cfie.reason)
}
// DefinitionLine is a single line in a checkout definition file.
type DefinitionLine struct {
Checksum string
FileSize int64
FilePath string
}
// DefinitionReader reads and parses a checkout definition
type DefinitionReader struct {
ctx context.Context
channel chan *DefinitionLine
reader *bufio.Reader
Err error
LineNumber int
}
var (
// This is a wider range than used in SHA256 sums, but there is no harm in accepting a few more ASCII letters.
validChecksumRegexp = regexp.MustCompile("^[a-zA-Z0-9]{16,}$")
)
// NewDefinitionReader creates a new DefinitionReader for the given reader.
func NewDefinitionReader(ctx context.Context, reader io.Reader) *DefinitionReader {
return &DefinitionReader{
ctx: ctx,
channel: make(chan *DefinitionLine),
reader: bufio.NewReader(reader),
}
}
// Read spins up a new goroutine for parsing the checkout definition.
// The returned channel will receive definition lines.
func (fr *DefinitionReader) Read() <-chan *DefinitionLine {
go func() {
defer close(fr.channel)
defer logrus.Debug("done reading request")
for {
line, err := fr.reader.ReadString('\n')
if err != nil && err != io.EOF {
fr.Err = FileInvalidError{
lineNumber: fr.LineNumber,
innerErr: err,
reason: fmt.Sprintf("I/O error: %v", err),
}
return
}
if err == io.EOF && line == "" {
return
}
if contextError := fr.ctx.Err(); contextError != nil {
fr.Err = fr.ctx.Err()
return
}
fr.LineNumber++
logrus.WithFields(logrus.Fields{
"line": line,
"number": fr.LineNumber,
}).Debug("read line")
line = strings.TrimSpace(line)
if line == "" {
continue
}
definitionLine, err := fr.parseLine(line)
if err != nil {
fr.Err = err
return
}
fr.channel <- definitionLine
}
}()
return fr.channel
}
func (fr *DefinitionReader) parseLine(line string) (*DefinitionLine, error) {
parts := strings.SplitN(strings.TrimSpace(line), " ", 3)
if len(parts) != 3 {
return nil, FileInvalidError{
lineNumber: fr.LineNumber,
reason: fmt.Sprintf("line should consist of three space-separated parts, not %d: %v",
len(parts), line),
}
}
checksum := parts[0]
if !validChecksumRegexp.MatchString(checksum) {
return nil, FileInvalidError{fr.LineNumber, nil, "invalid checksum"}
}
fileSize, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return nil, FileInvalidError{fr.LineNumber, err, "invalid file size"}
}
filePath := strings.TrimSpace(parts[2])
if path.IsAbs(filePath) {
return nil, FileInvalidError{fr.LineNumber, err, "no absolute paths allowed"}
}
if filePath != path.Clean(filePath) || strings.Contains(filePath, "..") {
return nil, FileInvalidError{fr.LineNumber, err, "paths must be clean and not have any .. in them."}
}
return &DefinitionLine{
Checksum: parts[0],
FileSize: fileSize,
FilePath: filePath,
}, nil
}