github.com/freiheit-com/kuberpult@v1.24.2-0.20240328135542-315d5630abe6/services/cd-service/pkg/fs/fs.go (about)

     1  /*This file is part of kuberpult.
     2  
     3  Kuberpult is free software: you can redistribute it and/or modify
     4  it under the terms of the Expat(MIT) License as published by
     5  the Free Software Foundation.
     6  
     7  Kuberpult is distributed in the hope that it will be useful,
     8  but WITHOUT ANY WARRANTY; without even the implied warranty of
     9  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    10  MIT License for more details.
    11  
    12  You should have received a copy of the MIT License
    13  along with kuberpult. If not, see <https://directory.fsf.org/wiki/License:Expat>.
    14  
    15  Copyright 2023 freiheit.com*/
    16  
    17  package fs
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"io/fs"
    23  	"os"
    24  	"path"
    25  	"strings"
    26  	"time"
    27  
    28  	billy "github.com/go-git/go-billy/v5"
    29  	git "github.com/libgit2/git2go/v34"
    30  )
    31  
    32  type fileInfo struct {
    33  	name string
    34  	size int64
    35  	mode os.FileMode
    36  }
    37  
    38  var _ os.FileInfo = (*fileInfo)(nil)
    39  
    40  func (f *fileInfo) Name() string {
    41  	return f.name
    42  }
    43  
    44  func (f *fileInfo) Size() int64 {
    45  	return f.size
    46  }
    47  
    48  func (f *fileInfo) Mode() os.FileMode {
    49  	return f.mode
    50  }
    51  
    52  func (f *fileInfo) IsDir() bool {
    53  	return f.mode.IsDir()
    54  }
    55  
    56  func (f *fileInfo) ModTime() time.Time {
    57  	return time.Time{}
    58  }
    59  
    60  func (f *fileInfo) Sys() interface{} {
    61  	return nil
    62  }
    63  
    64  type treeBuilderEntry interface {
    65  	load() error
    66  	osInfo() os.FileInfo
    67  	insert() (*git.Oid, git.Filemode, error)
    68  }
    69  
    70  type openMode int
    71  
    72  const (
    73  	closedMode openMode = iota
    74  	readMode
    75  	writeMode
    76  )
    77  
    78  type treeBuilderBlob struct {
    79  	name       string
    80  	repository *git.Repository
    81  	oid        *git.Oid
    82  	content    []byte
    83  	mode       openMode
    84  	pos        int
    85  }
    86  
    87  func (b *treeBuilderBlob) load() error {
    88  	if b.content == nil {
    89  		if b.oid != nil {
    90  			c, err := b.repository.LookupBlob(b.oid)
    91  			if err != nil {
    92  				return err
    93  			}
    94  			b.content = c.Contents()
    95  		}
    96  	}
    97  	return nil
    98  }
    99  
   100  func (b *treeBuilderBlob) insert() (*git.Oid, git.Filemode, error) {
   101  	if b.content == nil {
   102  		return b.oid, git.FilemodeBlob, nil
   103  	} else {
   104  		odb, err := b.repository.Odb()
   105  		if err != nil {
   106  			return nil, 0, err
   107  		}
   108  		oid, err := odb.Write(b.content, git.ObjectBlob)
   109  		if err != nil {
   110  			return nil, 0, err
   111  		}
   112  		return oid, git.FilemodeBlob, nil
   113  	}
   114  }
   115  
   116  func (b *treeBuilderBlob) osInfo() os.FileInfo {
   117  	b.load() //nolint: errcheck
   118  	return &fileInfo{
   119  		name: b.name,
   120  		size: int64(len(b.content)),
   121  		mode: 0666,
   122  	}
   123  }
   124  
   125  func (b *treeBuilderBlob) Name() string {
   126  	return b.name
   127  }
   128  
   129  func (b *treeBuilderBlob) Write(p []byte) (int, error) {
   130  	if b.mode != writeMode {
   131  		return 0, billy.ErrReadOnly
   132  	}
   133  	b.content = append(b.content[b.pos:], p...)
   134  	b.pos += len(p)
   135  	return len(p), nil
   136  }
   137  
   138  func (b *treeBuilderBlob) Read(p []byte) (int, error) {
   139  	err := b.load()
   140  	if err != nil {
   141  		return 0, err
   142  	}
   143  	n := copy(p, b.content[b.pos:])
   144  	b.pos += n
   145  	if n == 0 {
   146  		return 0, io.EOF
   147  	}
   148  	return n, nil
   149  }
   150  
   151  func (b *treeBuilderBlob) ReadAt(p []byte, off int64) (n int, err error) {
   152  	panic("ReadAt is not implemented")
   153  }
   154  
   155  func (b *treeBuilderBlob) Seek(offset int64, whence int) (int64, error) {
   156  	panic("Seek is not implemented")
   157  }
   158  
   159  func (b *treeBuilderBlob) Lock() error {
   160  	return billy.ErrNotSupported
   161  }
   162  
   163  func (b *treeBuilderBlob) Unlock() error {
   164  	return billy.ErrNotSupported
   165  }
   166  
   167  func (b *treeBuilderBlob) Truncate(s int64) error {
   168  	if b.mode != writeMode {
   169  		return billy.ErrReadOnly
   170  	}
   171  	b.content = b.content[0:s]
   172  	return nil
   173  }
   174  
   175  func (b *treeBuilderBlob) Close() error {
   176  	b.mode = closedMode
   177  	return nil
   178  }
   179  
   180  type treeBuilderSymlink struct {
   181  	name       string
   182  	target     string
   183  	oid        *git.Oid
   184  	repository *git.Repository
   185  }
   186  
   187  func (b *treeBuilderSymlink) load() error {
   188  	if b.target == "" {
   189  		if b.oid != nil {
   190  			bld, err := b.repository.LookupBlob(b.oid)
   191  			if err != nil {
   192  				return err
   193  			}
   194  			data := bld.Contents()
   195  			b.target = string(data)
   196  		}
   197  	}
   198  	return nil
   199  }
   200  
   201  func (b *treeBuilderSymlink) insert() (*git.Oid, git.Filemode, error) {
   202  	if b.target == "" {
   203  		return b.oid, git.FilemodeLink, nil
   204  	} else {
   205  		odb, err := b.repository.Odb()
   206  		if err != nil {
   207  			return nil, 0, err
   208  		}
   209  		oid, err := odb.Write([]byte(b.target), git.ObjectBlob)
   210  		if err != nil {
   211  			return nil, 0, err
   212  		}
   213  		return oid, git.FilemodeLink, nil
   214  	}
   215  }
   216  
   217  func (b *treeBuilderSymlink) osInfo() os.FileInfo {
   218  	return &fileInfo{
   219  		size: 0,
   220  		name: b.name,
   221  		mode: os.ModeSymlink | os.ModePerm,
   222  	}
   223  }
   224  
   225  type TreeBuilderFS struct {
   226  	info       fileInfo
   227  	entries    map[string]treeBuilderEntry
   228  	repository *git.Repository
   229  	oid        *git.Oid
   230  	parent     *TreeBuilderFS
   231  }
   232  
   233  func (t *TreeBuilderFS) load() error {
   234  	if t.entries == nil {
   235  		tree, err := t.repository.LookupTree(t.oid)
   236  		if err != nil {
   237  			return err
   238  		}
   239  		result := map[string]treeBuilderEntry{}
   240  		count := tree.EntryCount()
   241  		for i := uint64(0); i < count; i++ {
   242  			entry := tree.EntryByIndex(i)
   243  			switch entry.Filemode {
   244  			case git.FilemodeTree:
   245  				result[entry.Name] = &TreeBuilderFS{
   246  					entries: nil,
   247  					info: fileInfo{
   248  						size: 0,
   249  						name: entry.Name,
   250  						mode: os.ModeDir | os.ModePerm,
   251  					},
   252  					repository: t.repository,
   253  					parent:     t,
   254  					oid:        entry.Id,
   255  				}
   256  			case git.FilemodeBlob:
   257  				result[entry.Name] = &treeBuilderBlob{
   258  					content:    nil,
   259  					mode:       0,
   260  					pos:        0,
   261  					name:       entry.Name,
   262  					repository: t.repository,
   263  					oid:        entry.Id,
   264  				}
   265  			case git.FilemodeLink:
   266  				result[entry.Name] = &treeBuilderSymlink{
   267  					target:     "",
   268  					name:       entry.Name,
   269  					repository: t.repository,
   270  					oid:        entry.Id,
   271  				}
   272  			default:
   273  				return fmt.Errorf("unsupported file mode %d", entry.Filemode)
   274  			}
   275  		}
   276  		t.entries = result
   277  	}
   278  	return nil
   279  }
   280  
   281  func (t *TreeBuilderFS) insert() (*git.Oid, git.Filemode, error) {
   282  	if t.entries == nil {
   283  		// this tree wasn't modified, we can short-circuit it
   284  		return t.oid, git.FilemodeTree, nil
   285  	} else {
   286  		bld, err := t.repository.TreeBuilder()
   287  		if err != nil {
   288  			return nil, 0, err
   289  		}
   290  		defer bld.Free()
   291  		for name, entry := range t.entries {
   292  			oid, mode, err := entry.insert()
   293  			if err != nil {
   294  				return nil, 0, err
   295  			}
   296  			if oid == nil {
   297  				return nil, 0, fmt.Errorf("Oid is zero for %s %#v", name, entry)
   298  			}
   299  			err = bld.Insert(name, oid, mode)
   300  			if err != nil {
   301  				return nil, 0, err
   302  			}
   303  		}
   304  		oid, err := bld.Write()
   305  		if err != nil {
   306  			return nil, 0, err
   307  		}
   308  		return oid, git.FilemodeTree, nil
   309  	}
   310  }
   311  
   312  func (t *TreeBuilderFS) Insert() (*git.Oid, error) {
   313  	oid, _, err := t.insert()
   314  	return oid, err
   315  }
   316  
   317  func (t *TreeBuilderFS) osInfo() os.FileInfo {
   318  	return &t.info
   319  }
   320  
   321  func (t *TreeBuilderFS) Create(filename string) (billy.File, error) {
   322  	return t.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0666)
   323  }
   324  
   325  func (t *TreeBuilderFS) Open(filename string) (billy.File, error) {
   326  	return t.OpenFile(filename, os.O_RDONLY, 0666)
   327  }
   328  
   329  func (t *TreeBuilderFS) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
   330  	node, name, err := t.traverse(filename, false)
   331  	if err != nil {
   332  		return nil, err
   333  	}
   334  	if err := node.load(); err != nil {
   335  		return nil, err
   336  	}
   337  	if entry, ok := node.entries[name]; ok {
   338  		// found
   339  		if file, ok := entry.(*treeBuilderBlob); ok {
   340  			if file.mode == closedMode {
   341  				if flag&os.O_RDONLY != 0 {
   342  					file.mode = readMode
   343  				} else {
   344  					file.mode = writeMode
   345  				}
   346  				file.pos = 0
   347  			} else {
   348  				return nil, fmt.Errorf("file is already opened %q", filename)
   349  			}
   350  			return file, nil
   351  		} else {
   352  			return nil, fs.ErrInvalid
   353  		}
   354  	} else {
   355  		if flag&os.O_CREATE != 0 {
   356  			file := &treeBuilderBlob{
   357  				oid:        nil,
   358  				pos:        0,
   359  				name:       name,
   360  				repository: t.repository,
   361  				mode:       writeMode,
   362  				content:    []byte{},
   363  			}
   364  			node.entries[name] = file
   365  			return file, nil
   366  		} else {
   367  			return nil, fs.ErrNotExist
   368  		}
   369  	}
   370  }
   371  
   372  func (t *TreeBuilderFS) Stat(filename string) (os.FileInfo, error) {
   373  	node, rest, err := t.traverse(filename, false)
   374  	if err != nil {
   375  		return nil, err
   376  	}
   377  	if err := node.load(); err != nil {
   378  		return nil, err
   379  	}
   380  	if entry, ok := node.entries[rest]; ok {
   381  		return entry.osInfo(), nil
   382  	} else {
   383  		return nil, os.ErrNotExist
   384  	}
   385  }
   386  
   387  func (t *TreeBuilderFS) Rename(oldpath, newpath string) error {
   388  	panic("Rename is not implemented")
   389  }
   390  
   391  func (t *TreeBuilderFS) Remove(filename string) error {
   392  	node, rest, err := t.traverse(filename, false)
   393  	if err != nil {
   394  		return err
   395  	}
   396  	if err := node.load(); err != nil {
   397  		return err
   398  	}
   399  	delete(node.entries, rest)
   400  	return nil
   401  }
   402  
   403  func (t *TreeBuilderFS) Join(elem ...string) string {
   404  	return path.Join(elem...)
   405  }
   406  
   407  func (t *TreeBuilderFS) Root() string {
   408  	return ""
   409  }
   410  
   411  func removeTrailingSlashes(in string) string {
   412  	return strings.TrimRight(in, "/")
   413  }
   414  
   415  func (t *TreeBuilderFS) traverse(d string, createMissing bool) (*TreeBuilderFS, string, error) {
   416  	parts := strings.Split(removeTrailingSlashes(d), "/")
   417  	depth := 0
   418  	current := t
   419  	for {
   420  		d := parts[0]
   421  		parts = parts[1:]
   422  		if len(parts) == 0 {
   423  			return current, d, nil
   424  		}
   425  		if d == "." || d == "" {
   426  			continue
   427  		} else if d == ".." {
   428  			if current.parent != nil {
   429  				current = current.parent
   430  				depth -= 1
   431  			}
   432  		} else {
   433  			if err := current.load(); err != nil {
   434  				return nil, "", err
   435  			}
   436  			s := current.entries[d]
   437  			if s == nil {
   438  				if createMissing {
   439  					tree := NewEmptyTreeBuildFS(t.repository)
   440  					tree.info.name = d
   441  					tree.parent = current
   442  					current.entries[d] = tree
   443  					s = tree
   444  				} else {
   445  					return nil, "", fs.ErrNotExist
   446  				}
   447  			}
   448  			if u, ok := s.(*TreeBuilderFS); ok {
   449  				current = u
   450  				depth += 1
   451  			} else if l, ok := s.(*treeBuilderSymlink); ok {
   452  				if err := l.load(); err != nil {
   453  					return nil, "", err
   454  				}
   455  				ts := strings.Split(removeTrailingSlashes(l.target), "/")
   456  				parts = append(ts, parts...)
   457  				depth += 1
   458  			} else {
   459  				return nil, "", fs.ErrInvalid
   460  			}
   461  		}
   462  	}
   463  }
   464  
   465  func (t *TreeBuilderFS) Chroot(dir string) (billy.Filesystem, error) {
   466  	node, _, err := t.traverse(dir+"/.", false)
   467  	return node, err
   468  }
   469  
   470  func (t *TreeBuilderFS) TempFile(dir, prefix string) (billy.File, error) {
   471  	return nil, billy.ErrReadOnly
   472  }
   473  
   474  func (t *TreeBuilderFS) ReadDir(dir string) ([]os.FileInfo, error) {
   475  	node, _, err := t.traverse(dir+"/.", false)
   476  	if err != nil {
   477  		if err == fs.ErrNotExist {
   478  			return nil, nil
   479  		}
   480  		return nil, err
   481  	}
   482  	if err := node.load(); err != nil {
   483  		return nil, err
   484  	}
   485  	result := make([]os.FileInfo, 0, len(node.entries))
   486  	for _, entry := range node.entries {
   487  		result = append(result, entry.osInfo())
   488  	}
   489  	return result, nil
   490  
   491  }
   492  
   493  func (t *TreeBuilderFS) MkdirAll(dir string, perm os.FileMode) error {
   494  	_, _, err := t.traverse(dir+"/.", true)
   495  	return err
   496  }
   497  
   498  func (t *TreeBuilderFS) Lstat(path string) (os.FileInfo, error) {
   499  	// TODO(HVG): implement this to support actual symlinkk (https://github.com/freiheit-com/kuberpult/issues/1046)
   500  	return t.Stat(path)
   501  }
   502  
   503  func (t *TreeBuilderFS) Readlink(path string) (string, error) {
   504  	node, rest, err := t.traverse(path, false)
   505  	if err != nil {
   506  		return "", err
   507  	}
   508  	if err = node.load(); err != nil {
   509  		return "", err
   510  	}
   511  	if entry, ok := node.entries[rest]; ok {
   512  		if lnk, ok := entry.(*treeBuilderSymlink); ok {
   513  			if err := lnk.load(); err != nil {
   514  				return "", err
   515  			} else {
   516  				return lnk.target, nil
   517  			}
   518  		} else {
   519  			return "", fs.ErrInvalid
   520  		}
   521  	} else {
   522  		return "", fs.ErrNotExist
   523  	}
   524  }
   525  
   526  func (t *TreeBuilderFS) Symlink(target, filename string) error {
   527  	node, name, err := t.traverse(filename, false)
   528  	if err != nil {
   529  		return err
   530  	}
   531  	if err := node.load(); err != nil {
   532  		return err
   533  	}
   534  	link := &treeBuilderSymlink{
   535  		oid:        nil,
   536  		name:       name,
   537  		target:     target,
   538  		repository: t.repository,
   539  	}
   540  	node.entries[name] = link
   541  	return nil
   542  }
   543  
   544  func NewEmptyTreeBuildFS(repo *git.Repository) *TreeBuilderFS {
   545  	return &TreeBuilderFS{
   546  		oid:    nil,
   547  		parent: nil,
   548  		info: fileInfo{
   549  			name: "",
   550  			size: 0,
   551  			mode: os.ModeDir | os.ModePerm,
   552  		},
   553  		repository: repo,
   554  		entries:    map[string]treeBuilderEntry{},
   555  	}
   556  }
   557  
   558  func NewTreeBuildFS(repo *git.Repository, oid *git.Oid) *TreeBuilderFS {
   559  	return &TreeBuilderFS{
   560  		entries: nil,
   561  		parent:  nil,
   562  		info: fileInfo{
   563  			name: "",
   564  			size: 0,
   565  			mode: os.ModeDir | os.ModePerm,
   566  		},
   567  		repository: repo,
   568  		oid:        oid,
   569  	}
   570  }
   571  
   572  var _ billy.Filesystem = (*TreeBuilderFS)(nil)