github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/pkg/statik/statik.go (about) 1 // Copyright 2014 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Package contains a program that generates code to register 16 // a directory and its contents as zip data for statik file system. 17 package main 18 19 import ( 20 "bufio" 21 "bytes" 22 "crypto/sha256" 23 "encoding/hex" 24 "encoding/pem" 25 "errors" 26 "flag" 27 "fmt" 28 "io" 29 "net/http" 30 "os" 31 "path" 32 "path/filepath" 33 "sort" 34 "strconv" 35 "strings" 36 37 "github.com/andybalholm/brotli" 38 humanize "github.com/dustin/go-humanize" 39 ) 40 41 const ( 42 namePackage = "statik" 43 nameSourceFile = "statik.go" 44 ) 45 46 var ( 47 flagSrc = flag.String("src", path.Join(".", "public"), "The path of the source directory.") 48 flagDest = flag.String("dest", ".", "The destination path of the generated package.") 49 flagExternals = flag.String("externals", "", "File containing a description of externals assets to download.") 50 flagForce = flag.Bool("f", false, "Overwrite destination file if it already exists.") 51 ) 52 53 var ( 54 errExternalsMalformed = errors.New("assets externals file malformed") 55 ) 56 57 type asset struct { 58 name string 59 size int64 60 url string 61 data []byte 62 sha256 []byte 63 } 64 65 func main() { 66 flag.Parse() 67 68 destDir := path.Join(*flagDest, namePackage) 69 destFilename := path.Join(destDir, nameSourceFile) 70 71 file, noChange, err := generateSource(destFilename, *flagSrc, *flagExternals) 72 if err != nil { 73 exitWithError(err) 74 } 75 76 if !noChange { 77 err = os.MkdirAll(destDir, 0755) 78 if err != nil { 79 exitWithError(err) 80 } 81 82 src := file.Name() 83 84 hSrc, err := shasum(src) 85 if err != nil { 86 exitWithError(err) 87 } 88 hDest, err := shasum(destFilename) 89 if err != nil { 90 exitWithError(err) 91 } 92 93 if !bytes.Equal(hSrc, hDest) { 94 err = rename(src, destFilename) 95 if err != nil { 96 exitWithError(err) 97 } 98 fmt.Println("asset file updated successfully") 99 } else { 100 fmt.Println("asset file left unchanged") 101 } 102 } else { 103 fmt.Println("asset file left unchanged") 104 } 105 } 106 107 func shasum(file string) ([]byte, error) { 108 h := sha256.New() 109 f, err := os.Open(file) 110 if err != nil { 111 return nil, err 112 } 113 defer f.Close() 114 if _, err := io.Copy(h, f); err != nil { 115 return nil, err 116 } 117 return h.Sum(nil), nil 118 } 119 120 // rename tries to os.Rename, but fall backs to copying from src 121 // to dest and unlink the source if os.Rename fails. 122 func rename(src, dest string) error { 123 // Try to rename generated source. 124 if err := os.Rename(src, dest); err == nil { 125 return nil 126 } 127 // If the rename failed (might do so due to temporary file residing on a 128 // different device), try to copy byte by byte. 129 rc, err := os.Open(src) 130 if err != nil { 131 return err 132 } 133 defer func() { 134 rc.Close() 135 os.Remove(src) // ignore the error, source is in tmp. 136 }() 137 138 if _, err = os.Stat(dest); !os.IsNotExist(err) { 139 if *flagForce { 140 if err = os.Remove(dest); err != nil { 141 return fmt.Errorf("file %q could not be deleted", dest) 142 } 143 } else { 144 return fmt.Errorf("file %q already exists; use -f to overwrite", dest) 145 } 146 } 147 148 wc, err := os.Create(dest) 149 if err != nil { 150 return err 151 } 152 defer wc.Close() 153 154 if _, err = io.Copy(wc, rc); err != nil { 155 // Delete remains of failed copy attempt. 156 os.Remove(dest) 157 } 158 return err 159 } 160 161 func loadAsset(name, srcPath string) (*asset, error) { 162 data := new(bytes.Buffer) 163 164 f, err := os.Open(name) 165 if err != nil { 166 return nil, err 167 } 168 defer f.Close() 169 170 h := sha256.New() 171 r := io.TeeReader(f, h) 172 size, err := io.Copy(data, r) 173 if err != nil { 174 return nil, err 175 } 176 177 relPath, err := filepath.Rel(srcPath, name) 178 if err != nil { 179 return nil, err 180 } 181 182 return &asset{ 183 name: path.Join("/", filepath.ToSlash(relPath)), 184 size: size, 185 sha256: h.Sum(nil), 186 data: data.Bytes(), 187 }, nil 188 } 189 190 // Walks on the source path and generates source code 191 // that contains source directory's contents as zip contents. 192 // Generates source registers generated zip contents data to 193 // be read by the statik/fs HTTP file system. 194 func generateSource(destFilename, srcPath, externalsFile string) (f *os.File, noChange bool, err error) { 195 var assets []*asset 196 197 currentAssets, err := readCurrentAssets(destFilename) 198 if err != nil { 199 return 200 } 201 202 doneCh := make(chan error) 203 filesCh := make(chan string) 204 assetsCh := make(chan *asset) 205 206 go func() { 207 defer close(filesCh) 208 err = filepath.Walk(srcPath, func(name string, fi os.FileInfo, err error) error { 209 if err != nil { 210 return err 211 } 212 // Ignore directories and hidden assets. 213 // No entry is needed for directories in a zip file. 214 // Each file is represented with a path, no directory 215 // entities are required to build the hierarchy. 216 if !fi.IsDir() && !strings.HasPrefix(fi.Name(), ".") { 217 filesCh <- name 218 } 219 return nil 220 }) 221 if err != nil { 222 doneCh <- err 223 } 224 }() 225 226 for i := 0; i < 16; i++ { 227 go func() { 228 for name := range filesCh { 229 asset, err := loadAsset(name, srcPath) 230 if err != nil { 231 doneCh <- err 232 return 233 } 234 assetsCh <- asset 235 } 236 doneCh <- nil 237 }() 238 } 239 240 go func() { 241 defer close(assetsCh) 242 for i := 0; i < 16; i++ { 243 if err = <-doneCh; err != nil { 244 return 245 } 246 } 247 }() 248 249 for a := range assetsCh { 250 assets = append(assets, a) 251 } 252 if err != nil { 253 return 254 } 255 256 if externalsFile != "" { 257 var exts []*asset 258 exts, err = downloadExternals(externalsFile, currentAssets) 259 if err != nil { 260 return 261 } 262 assets = append(assets, exts...) 263 } 264 265 sort.Slice(assets, func(i, j int) bool { 266 return assets[i].name < assets[j].name 267 }) 268 269 if len(assets) == len(currentAssets) { 270 noChange = true 271 for i, a := range assets { 272 old := currentAssets[i] 273 if old.name != a.name || !bytes.Equal(old.sha256, a.sha256) { 274 noChange = false 275 break 276 } 277 } 278 } 279 if noChange { 280 return 281 } 282 283 f, err = os.CreateTemp("", namePackage) 284 if err != nil { 285 return 286 } 287 288 _, err = fmt.Fprintf(f, `// Code generated by statik. DO NOT EDIT. 289 290 package %s 291 292 import ( 293 fs "github.com/cozy/cozy-stack/pkg/assets/statik" 294 ) 295 296 func init() { 297 data := `, namePackage) 298 if err != nil { 299 return 300 } 301 302 _, err = fmt.Fprint(f, "`") 303 if err != nil { 304 return 305 } 306 307 err = printData(f, assets) 308 if err != nil { 309 return 310 } 311 312 _, err = fmt.Fprint(f, "`") 313 if err != nil { 314 return 315 } 316 _, err = fmt.Fprint(f, ` 317 fs.Register(data) 318 } 319 `) 320 if err != nil { 321 return 322 } 323 324 return 325 } 326 327 func downloadExternals(filename string, currentAssets []*asset) (newAssets []*asset, err error) { 328 externalAssets, err := parseExternalsFile(filename) 329 if err != nil { 330 return 331 } 332 333 currentAssetsMap := make(map[string]*asset) 334 for _, a := range currentAssets { 335 currentAssetsMap[a.name] = a 336 } 337 338 for _, externalAsset := range externalAssets { 339 var newAsset *asset 340 if a, ok := currentAssetsMap[externalAsset.name]; ok && bytes.Equal(a.sha256, externalAsset.sha256) { 341 newAsset = a 342 } else { 343 fmt.Fprintf(os.Stdout, "downloading %q... ", externalAsset.name) 344 newAsset, err = downloadExternal(externalAsset) 345 if err != nil { 346 return 347 } 348 fmt.Fprintf(os.Stdout, "ok (%s)\n", humanize.Bytes(uint64(newAsset.size))) 349 } 350 newAssets = append(newAssets, newAsset) 351 } 352 353 return 354 } 355 356 func parseExternalsFile(filename string) (assets []*asset, err error) { 357 f, err := os.Open(filename) 358 if err != nil { 359 return 360 } 361 defer func() { 362 if errc := f.Close(); errc != nil && err == nil { 363 err = errc 364 } 365 }() 366 367 var a *asset 368 scanner := bufio.NewScanner(f) 369 for scanner.Scan() { 370 line := scanner.Text() 371 if len(line) > 0 && line[0] == '#' { 372 continue 373 } 374 fields := strings.Fields(line) 375 switch len(fields) { 376 case 0: 377 if a != nil { 378 return nil, errExternalsMalformed 379 } 380 case 2: 381 if a == nil { 382 a = new(asset) 383 } 384 k, v := fields[0], fields[1] 385 switch strings.ToLower(k) { 386 case "name": 387 a.name = path.Join("/", v) 388 case "url": 389 a.url = v 390 case "sha256": 391 a.sha256, err = hex.DecodeString(v) 392 if err != nil { 393 return nil, errExternalsMalformed 394 } 395 } 396 default: 397 return nil, errExternalsMalformed 398 } 399 if a != nil && a.name != "" && a.url != "" && len(a.sha256) > 0 { 400 assets = append(assets, a) 401 a = nil 402 } 403 } 404 if errs := scanner.Err(); errs != nil { 405 return nil, errs 406 } 407 408 return 409 } 410 411 func downloadExternal(ext *asset) (f *asset, err error) { 412 res, err := http.Get(ext.url) 413 if err != nil { 414 return nil, err 415 } 416 defer res.Body.Close() 417 418 if res.StatusCode != http.StatusOK { 419 return nil, fmt.Errorf("could not fetch external assets %q: received status \"%d %s\"", 420 ext.url, res.StatusCode, res.Status) 421 } 422 423 h := sha256.New() 424 r := io.TeeReader(res.Body, h) 425 426 data, err := io.ReadAll(r) 427 if err != nil { 428 return nil, fmt.Errorf("could not fetch external asset: %s", err) 429 } 430 431 if sum := h.Sum(nil); !bytes.Equal(sum, ext.sha256) { 432 return nil, fmt.Errorf("shasum does not match: expected %x got %x", 433 ext.sha256, sum) 434 } 435 436 return &asset{ 437 data: data, 438 name: ext.name, 439 size: int64(len(data)), 440 sha256: ext.sha256, 441 }, nil 442 } 443 444 func readCurrentAssets(filename string) (assets []*asset, err error) { 445 statikFile, err := os.ReadFile(filename) 446 if err != nil && !os.IsNotExist(err) { 447 return nil, err 448 } 449 450 var zippedData []byte 451 if len(statikFile) > 0 { 452 i := bytes.Index(statikFile, []byte("`")) 453 if i >= 0 { 454 j := bytes.Index(statikFile[i+1:], []byte("`")) 455 if i >= 0 && j > i { 456 zippedData = statikFile[i+1 : i+j] 457 } 458 } 459 } 460 461 for { 462 block, rest := pem.Decode(zippedData) 463 if block == nil { 464 break 465 } 466 var size int64 467 size, err = strconv.ParseInt(block.Headers["Size"], 10, 64) 468 if err != nil { 469 return 470 } 471 br := brotli.NewReader(bytes.NewReader(block.Bytes)) 472 var data []byte 473 h := sha256.New() 474 r := io.TeeReader(br, h) 475 data, err = io.ReadAll(r) 476 if err != nil { 477 return 478 } 479 name := block.Headers["Name"] 480 assets = append(assets, &asset{ 481 name: name, 482 size: size, 483 data: data, 484 sha256: h.Sum(nil), 485 }) 486 zippedData = rest 487 } 488 return 489 } 490 491 // printData converts contents to a string literal. 492 func printData(dest io.Writer, assets []*asset) error { 493 quality := brotli.BestCompression 494 if lvl := os.Getenv("BROTLI_LEVEL"); lvl != "" { 495 level, err := strconv.Atoi(lvl) 496 if err == nil { 497 quality = level 498 } 499 } 500 for _, f := range assets { 501 buf := new(bytes.Buffer) 502 bw := brotli.NewWriterLevel(buf, quality) 503 if _, err := io.Copy(bw, bytes.NewReader(f.data)); err != nil { 504 return err 505 } 506 if err := bw.Close(); err != nil { 507 return err 508 } 509 err := pem.Encode(dest, &pem.Block{ 510 Type: "COZY ASSET", 511 Bytes: buf.Bytes(), 512 Headers: map[string]string{ 513 "Name": f.name, 514 "Size": strconv.FormatInt(f.size, 10), 515 }, 516 }) 517 if err != nil { 518 return err 519 } 520 } 521 return nil 522 } 523 524 // Prints out the error message and exists with a non-success signal. 525 func exitWithError(err error) { 526 fmt.Println(err) 527 os.Exit(1) 528 }