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)