github.com/SaurabhDubey-Groww/go-cloud@v0.0.0-20221124105541-b26c29285fd8/blob/fileblob/fileblob.go (about) 1 // Copyright 2018 The Go Cloud Development Kit Authors 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 // https://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 fileblob provides a blob implementation that uses the filesystem. 16 // Use OpenBucket to construct a *blob.Bucket. 17 // 18 // By default fileblob stores blob metadata in 'sidecar files' under the original 19 // filename but an additional ".attrs" suffix. 20 // That behaviour can be changed via Options.Metadata; 21 // writing of those metadata files can be suppressed by setting it to 22 // 'MetadataDontWrite' or its equivalent "metadata=skip" in the URL for the opener. 23 // In any case, absent any stored metadata many blob.Attributes fields 24 // will be set to default values. 25 // 26 // # URLs 27 // 28 // For blob.OpenBucket, fileblob registers for the scheme "file". 29 // To customize the URL opener, or for more details on the URL format, 30 // see URLOpener. 31 // See https://gocloud.dev/concepts/urls/ for background information. 32 // 33 // # Escaping 34 // 35 // Go CDK supports all UTF-8 strings; to make this work with services lacking 36 // full UTF-8 support, strings must be escaped (during writes) and unescaped 37 // (during reads). The following escapes are performed for fileblob: 38 // - Blob keys: ASCII characters 0-31 are escaped to "__0x<hex>__". 39 // If os.PathSeparator != "/", it is also escaped. 40 // Additionally, the "/" in "../", the trailing "/" in "//", and a trailing 41 // "/" is key names are escaped in the same way. 42 // On Windows, the characters "<>:"|?*" are also escaped. 43 // 44 // # As 45 // 46 // fileblob exposes the following types for As: 47 // - Bucket: os.FileInfo 48 // - Error: *os.PathError 49 // - ListObject: os.FileInfo 50 // - Reader: io.Reader 51 // - ReaderOptions.BeforeRead: *os.File 52 // - Attributes: os.FileInfo 53 // - CopyOptions.BeforeCopy: *os.File 54 // - WriterOptions.BeforeWrite: *os.File 55 package fileblob // import "gocloud.dev/blob/fileblob" 56 57 import ( 58 "context" 59 "crypto/hmac" 60 "crypto/md5" 61 "crypto/sha256" 62 "encoding/base64" 63 "errors" 64 "fmt" 65 "hash" 66 "io" 67 "io/fs" 68 "io/ioutil" 69 "net/url" 70 "os" 71 "path/filepath" 72 "strconv" 73 "strings" 74 "time" 75 76 "gocloud.dev/blob" 77 "gocloud.dev/blob/driver" 78 "gocloud.dev/gcerrors" 79 "gocloud.dev/internal/escape" 80 "gocloud.dev/internal/gcerr" 81 ) 82 83 const defaultPageSize = 1000 84 85 func init() { 86 blob.DefaultURLMux().RegisterBucket(Scheme, &URLOpener{}) 87 } 88 89 // Scheme is the URL scheme fileblob registers its URLOpener under on 90 // blob.DefaultMux. 91 const Scheme = "file" 92 93 // URLOpener opens file bucket URLs like "file:///foo/bar/baz". 94 // 95 // The URL's host is ignored unless it is ".", which is used to signal a 96 // relative path. For example, "file://./../.." uses "../.." as the path. 97 // 98 // If os.PathSeparator != "/", any leading "/" from the path is dropped 99 // and remaining '/' characters are converted to os.PathSeparator. 100 // 101 // The following query parameters are supported: 102 // 103 // - create_dir: (any non-empty value) the directory is created (using os.MkDirAll) 104 // if it does not already exist. 105 // - base_url: the base URL to use to construct signed URLs; see URLSignerHMAC 106 // - secret_key_path: path to read for the secret key used to construct signed URLs; 107 // see URLSignerHMAC 108 // - metadata: if set to "skip", won't write metadata such as blob.Attributes 109 // as per the package docstring 110 // 111 // If either of base_url / secret_key_path are provided, both must be. 112 // 113 // - file:///a/directory 114 // -> Passes "/a/directory" to OpenBucket. 115 // - file://localhost/a/directory 116 // -> Also passes "/a/directory". 117 // - file://./../.. 118 // -> The hostname is ".", signaling a relative path; passes "../..". 119 // - file:///c:/foo/bar on Windows. 120 // -> Passes "c:\foo\bar". 121 // - file://localhost/c:/foo/bar on Windows. 122 // -> Also passes "c:\foo\bar". 123 // - file:///a/directory?base_url=/show&secret_key_path=secret.key 124 // -> Passes "/a/directory" to OpenBucket, and sets Options.URLSigner 125 // to a URLSignerHMAC initialized with base URL "/show" and secret key 126 // bytes read from the file "secret.key". 127 type URLOpener struct { 128 // Options specifies the default options to pass to OpenBucket. 129 Options Options 130 } 131 132 // OpenBucketURL opens a blob.Bucket based on u. 133 func (o *URLOpener) OpenBucketURL(ctx context.Context, u *url.URL) (*blob.Bucket, error) { 134 path := u.Path 135 // Hostname == "." means a relative path, so drop the leading "/". 136 // Also drop the leading "/" on Windows. 137 if u.Host == "." || os.PathSeparator != '/' { 138 path = strings.TrimPrefix(path, "/") 139 } 140 opts, err := o.forParams(ctx, u.Query()) 141 if err != nil { 142 return nil, fmt.Errorf("open bucket %v: %v", u, err) 143 } 144 return OpenBucket(filepath.FromSlash(path), opts) 145 } 146 147 var recognizedParams = map[string]bool{ 148 "create_dir": true, 149 "base_url": true, 150 "secret_key_path": true, 151 "metadata": true, 152 } 153 154 type metadataOption string // Not exported as subject to change. 155 156 // Settings for Options.Metadata. 157 const ( 158 // Metadata gets written to a separate file. 159 MetadataInSidecar metadataOption = "" 160 // Writes won't carry metadata, as per the package docstring. 161 MetadataDontWrite metadataOption = "skip" 162 ) 163 164 func (o *URLOpener) forParams(ctx context.Context, q url.Values) (*Options, error) { 165 for k := range q { 166 if _, ok := recognizedParams[k]; !ok { 167 return nil, fmt.Errorf("invalid query parameter %q", k) 168 } 169 } 170 opts := new(Options) 171 *opts = o.Options 172 173 // Note: can't just use q.Get, because then we can't distinguish between 174 // "not set" (we should leave opts alone) vs "set to empty string" (which is 175 // one of the legal values, we should override opts). 176 metadataVal := q["metadata"] 177 if len(metadataVal) > 0 { 178 switch metadataOption(metadataVal[0]) { 179 case MetadataDontWrite: 180 opts.Metadata = MetadataDontWrite 181 case MetadataInSidecar: 182 opts.Metadata = MetadataInSidecar 183 default: 184 return nil, errors.New("fileblob.OpenBucket: unsupported value for query parameter 'metadata'") 185 } 186 } 187 if q.Get("create_dir") != "" { 188 opts.CreateDir = true 189 } 190 baseURL := q.Get("base_url") 191 keyPath := q.Get("secret_key_path") 192 if (baseURL == "") != (keyPath == "") { 193 return nil, errors.New("must supply both base_url and secret_key_path query parameters") 194 } 195 if baseURL != "" { 196 burl, err := url.Parse(baseURL) 197 if err != nil { 198 return nil, err 199 } 200 sk, err := ioutil.ReadFile(keyPath) 201 if err != nil { 202 return nil, err 203 } 204 opts.URLSigner = NewURLSignerHMAC(burl, sk) 205 } 206 return opts, nil 207 } 208 209 // Options sets options for constructing a *blob.Bucket backed by fileblob. 210 type Options struct { 211 // URLSigner implements signing URLs (to allow access to a resource without 212 // further authorization) and verifying that a given URL is unexpired and 213 // contains a signature produced by the URLSigner. 214 // URLSigner is only required for utilizing the SignedURL API. 215 URLSigner URLSigner 216 217 // If true, create the directory backing the Bucket if it does not exist 218 // (using os.MkdirAll). 219 CreateDir bool 220 221 // Refers to the strategy for how to deal with metadata (such as blob.Attributes). 222 // For supported values please see the Metadata* constants. 223 // If left unchanged, 'MetadataInSidecar' will be used. 224 Metadata metadataOption 225 } 226 227 type bucket struct { 228 dir string 229 opts *Options 230 } 231 232 // openBucket creates a driver.Bucket that reads and writes to dir. 233 // dir must exist. 234 func openBucket(dir string, opts *Options) (driver.Bucket, error) { 235 if opts == nil { 236 opts = &Options{} 237 } 238 absdir, err := filepath.Abs(dir) 239 if err != nil { 240 return nil, fmt.Errorf("failed to convert %s into an absolute path: %v", dir, err) 241 } 242 info, err := os.Stat(absdir) 243 244 // Optionally, create the directory if it does not already exist. 245 if err != nil && opts.CreateDir && os.IsNotExist(err) { 246 err = os.MkdirAll(absdir, os.FileMode(0777)) 247 if err != nil { 248 return nil, fmt.Errorf("tried to create directory but failed: %v", err) 249 } 250 info, err = os.Stat(absdir) 251 } 252 if err != nil { 253 return nil, err 254 } 255 if !info.IsDir() { 256 return nil, fmt.Errorf("%s is not a directory", absdir) 257 } 258 return &bucket{dir: absdir, opts: opts}, nil 259 } 260 261 // OpenBucket creates a *blob.Bucket backed by the filesystem and rooted at 262 // dir, which must exist. See the package documentation for an example. 263 func OpenBucket(dir string, opts *Options) (*blob.Bucket, error) { 264 drv, err := openBucket(dir, opts) 265 if err != nil { 266 return nil, err 267 } 268 return blob.NewBucket(drv), nil 269 } 270 271 func (b *bucket) Close() error { 272 return nil 273 } 274 275 // escapeKey does all required escaping for UTF-8 strings to work the filesystem. 276 func escapeKey(s string) string { 277 s = escape.HexEscape(s, func(r []rune, i int) bool { 278 c := r[i] 279 switch { 280 case c < 32: 281 return true 282 // We're going to replace '/' with os.PathSeparator below. In order for this 283 // to be reversible, we need to escape raw os.PathSeparators. 284 case os.PathSeparator != '/' && c == os.PathSeparator: 285 return true 286 // For "../", escape the trailing slash. 287 case i > 1 && c == '/' && r[i-1] == '.' && r[i-2] == '.': 288 return true 289 // For "//", escape the trailing slash. 290 case i > 0 && c == '/' && r[i-1] == '/': 291 return true 292 // Escape the trailing slash in a key. 293 case c == '/' && i == len(r)-1: 294 return true 295 // https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file 296 case os.PathSeparator == '\\' && (c == '>' || c == '<' || c == ':' || c == '"' || c == '|' || c == '?' || c == '*'): 297 return true 298 } 299 return false 300 }) 301 // Replace "/" with os.PathSeparator if needed, so that the local filesystem 302 // can use subdirectories. 303 if os.PathSeparator != '/' { 304 s = strings.Replace(s, "/", string(os.PathSeparator), -1) 305 } 306 return s 307 } 308 309 // unescapeKey reverses escapeKey. 310 func unescapeKey(s string) string { 311 if os.PathSeparator != '/' { 312 s = strings.Replace(s, string(os.PathSeparator), "/", -1) 313 } 314 s = escape.HexUnescape(s) 315 return s 316 } 317 318 func (b *bucket) ErrorCode(err error) gcerrors.ErrorCode { 319 switch { 320 case os.IsNotExist(err): 321 return gcerrors.NotFound 322 default: 323 return gcerrors.Unknown 324 } 325 } 326 327 // path returns the full path for a key 328 func (b *bucket) path(key string) (string, error) { 329 path := filepath.Join(b.dir, escapeKey(key)) 330 if strings.HasSuffix(path, attrsExt) { 331 return "", errAttrsExt 332 } 333 return path, nil 334 } 335 336 // forKey returns the full path, os.FileInfo, and attributes for key. 337 func (b *bucket) forKey(key string) (string, os.FileInfo, *xattrs, error) { 338 path, err := b.path(key) 339 if err != nil { 340 return "", nil, nil, err 341 } 342 info, err := os.Stat(path) 343 if err != nil { 344 return "", nil, nil, err 345 } 346 if info.IsDir() { 347 return "", nil, nil, os.ErrNotExist 348 } 349 xa, err := getAttrs(path) 350 if err != nil { 351 return "", nil, nil, err 352 } 353 return path, info, &xa, nil 354 } 355 356 // ListPaged implements driver.ListPaged. 357 func (b *bucket) ListPaged(ctx context.Context, opts *driver.ListOptions) (*driver.ListPage, error) { 358 359 var pageToken string 360 if len(opts.PageToken) > 0 { 361 pageToken = string(opts.PageToken) 362 } 363 pageSize := opts.PageSize 364 if pageSize == 0 { 365 pageSize = defaultPageSize 366 } 367 // If opts.Delimiter != "", lastPrefix contains the last "directory" key we 368 // added. It is used to avoid adding it again; all files in this "directory" 369 // are collapsed to the single directory entry. 370 var lastPrefix string 371 var lastKeyAdded string 372 373 // If the Prefix contains a "/", we can set the root of the Walk 374 // to the path specified by the Prefix as any files below the path will not 375 // match the Prefix. 376 // Note that we use "/" explicitly and not os.PathSeparator, as the opts.Prefix 377 // is in the unescaped form. 378 root := b.dir 379 if i := strings.LastIndex(opts.Prefix, "/"); i > -1 { 380 root = filepath.Join(root, opts.Prefix[:i]) 381 } 382 383 // Do a full recursive scan of the root directory. 384 var result driver.ListPage 385 err := filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error { 386 if err != nil { 387 // Couldn't read this file/directory for some reason; just skip it. 388 return nil 389 } 390 // Skip the self-generated attribute files. 391 if strings.HasSuffix(path, attrsExt) { 392 return nil 393 } 394 // os.Walk returns the root directory; skip it. 395 if path == b.dir { 396 return nil 397 } 398 // Strip the <b.dir> prefix from path. 399 prefixLen := len(b.dir) 400 // Include the separator for non-root. 401 if b.dir != "/" { 402 prefixLen++ 403 } 404 path = path[prefixLen:] 405 // Unescape the path to get the key. 406 key := unescapeKey(path) 407 // Skip all directories. If opts.Delimiter is set, we'll create 408 // pseudo-directories later. 409 // Note that returning nil means that we'll still recurse into it; 410 // we're just not adding a result for the directory itself. 411 if info.IsDir() { 412 key += "/" 413 // Avoid recursing into subdirectories if the directory name already 414 // doesn't match the prefix; any files in it are guaranteed not to match. 415 if len(key) > len(opts.Prefix) && !strings.HasPrefix(key, opts.Prefix) { 416 return filepath.SkipDir 417 } 418 // Similarly, avoid recursing into subdirectories if we're making 419 // "directories" and all of the files in this subdirectory are guaranteed 420 // to collapse to a "directory" that we've already added. 421 if lastPrefix != "" && strings.HasPrefix(key, lastPrefix) { 422 return filepath.SkipDir 423 } 424 return nil 425 } 426 // Skip files/directories that don't match the Prefix. 427 if !strings.HasPrefix(key, opts.Prefix) { 428 return nil 429 } 430 var md5 []byte 431 if xa, err := getAttrs(path); err == nil { 432 // Note: we only have the MD5 hash for blobs that we wrote. 433 // For other blobs, md5 will remain nil. 434 md5 = xa.MD5 435 } 436 fi, err := info.Info() 437 if err != nil { 438 return err 439 } 440 asFunc := func(i interface{}) bool { 441 p, ok := i.(*os.FileInfo) 442 if !ok { 443 return false 444 } 445 *p = fi 446 return true 447 } 448 obj := &driver.ListObject{ 449 Key: key, 450 ModTime: fi.ModTime(), 451 Size: fi.Size(), 452 MD5: md5, 453 AsFunc: asFunc, 454 } 455 // If using Delimiter, collapse "directories". 456 if opts.Delimiter != "" { 457 // Strip the prefix, which may contain Delimiter. 458 keyWithoutPrefix := key[len(opts.Prefix):] 459 // See if the key still contains Delimiter. 460 // If no, it's a file and we just include it. 461 // If yes, it's a file in a "sub-directory" and we want to collapse 462 // all files in that "sub-directory" into a single "directory" result. 463 if idx := strings.Index(keyWithoutPrefix, opts.Delimiter); idx != -1 { 464 prefix := opts.Prefix + keyWithoutPrefix[0:idx+len(opts.Delimiter)] 465 // We've already included this "directory"; don't add it. 466 if prefix == lastPrefix { 467 return nil 468 } 469 // Update the object to be a "directory". 470 obj = &driver.ListObject{ 471 Key: prefix, 472 IsDir: true, 473 AsFunc: asFunc, 474 } 475 lastPrefix = prefix 476 } 477 } 478 // If there's a pageToken, skip anything before it. 479 if pageToken != "" && obj.Key <= pageToken { 480 return nil 481 } 482 // If we've already got a full page of results, set NextPageToken and stop. 483 // Unless the current object is a directory, in which case there may 484 // still be objects coming that are alphabetically before it (since 485 // we appended the delimiter). In that case, keep going; we'll trim the 486 // extra entries (if any) before returning. 487 if len(result.Objects) == pageSize && !obj.IsDir { 488 result.NextPageToken = []byte(result.Objects[pageSize-1].Key) 489 return io.EOF 490 } 491 result.Objects = append(result.Objects, obj) 492 // Normally, objects are added in the correct order (by Key). 493 // However, sometimes adding the file delimiter messes that up (e.g., 494 // if the file delimiter is later in the alphabet than the last character 495 // of a key). 496 // Detect if this happens and swap if needed. 497 if len(result.Objects) > 1 && obj.Key < lastKeyAdded { 498 i := len(result.Objects) - 1 499 result.Objects[i-1], result.Objects[i] = result.Objects[i], result.Objects[i-1] 500 lastKeyAdded = result.Objects[i].Key 501 } else { 502 lastKeyAdded = obj.Key 503 } 504 return nil 505 }) 506 if err != nil && err != io.EOF { 507 return nil, err 508 } 509 if len(result.Objects) > pageSize { 510 result.Objects = result.Objects[0:pageSize] 511 result.NextPageToken = []byte(result.Objects[pageSize-1].Key) 512 } 513 return &result, nil 514 } 515 516 // As implements driver.As. 517 func (b *bucket) As(i interface{}) bool { 518 p, ok := i.(*os.FileInfo) 519 if !ok { 520 return false 521 } 522 fi, err := os.Stat(b.dir) 523 if err != nil { 524 return false 525 } 526 *p = fi 527 return true 528 } 529 530 // As implements driver.ErrorAs. 531 func (b *bucket) ErrorAs(err error, i interface{}) bool { 532 if perr, ok := err.(*os.PathError); ok { 533 if p, ok := i.(**os.PathError); ok { 534 *p = perr 535 return true 536 } 537 } 538 return false 539 } 540 541 // Attributes implements driver.Attributes. 542 func (b *bucket) Attributes(ctx context.Context, key string) (*driver.Attributes, error) { 543 _, info, xa, err := b.forKey(key) 544 if err != nil { 545 return nil, err 546 } 547 return &driver.Attributes{ 548 CacheControl: xa.CacheControl, 549 ContentDisposition: xa.ContentDisposition, 550 ContentEncoding: xa.ContentEncoding, 551 ContentLanguage: xa.ContentLanguage, 552 ContentType: xa.ContentType, 553 Metadata: xa.Metadata, 554 // CreateTime left as the zero time. 555 ModTime: info.ModTime(), 556 Size: info.Size(), 557 MD5: xa.MD5, 558 ETag: fmt.Sprintf("\"%x-%x\"", info.ModTime().UnixNano(), info.Size()), 559 AsFunc: func(i interface{}) bool { 560 p, ok := i.(*os.FileInfo) 561 if !ok { 562 return false 563 } 564 *p = info 565 return true 566 }, 567 }, nil 568 } 569 570 // NewRangeReader implements driver.NewRangeReader. 571 func (b *bucket) NewRangeReader(ctx context.Context, key string, offset, length int64, opts *driver.ReaderOptions) (driver.Reader, error) { 572 path, info, xa, err := b.forKey(key) 573 if err != nil { 574 return nil, err 575 } 576 f, err := os.Open(path) 577 if err != nil { 578 return nil, err 579 } 580 if opts.BeforeRead != nil { 581 if err := opts.BeforeRead(func(i interface{}) bool { 582 p, ok := i.(**os.File) 583 if !ok { 584 return false 585 } 586 *p = f 587 return true 588 }); err != nil { 589 return nil, err 590 } 591 } 592 if offset > 0 { 593 if _, err := f.Seek(offset, io.SeekStart); err != nil { 594 return nil, err 595 } 596 } 597 r := io.Reader(f) 598 if length >= 0 { 599 r = io.LimitReader(r, length) 600 } 601 return &reader{ 602 r: r, 603 c: f, 604 attrs: driver.ReaderAttributes{ 605 ContentType: xa.ContentType, 606 ModTime: info.ModTime(), 607 Size: info.Size(), 608 }, 609 }, nil 610 } 611 612 type reader struct { 613 r io.Reader 614 c io.Closer 615 attrs driver.ReaderAttributes 616 } 617 618 func (r *reader) Read(p []byte) (int, error) { 619 if r.r == nil { 620 return 0, io.EOF 621 } 622 return r.r.Read(p) 623 } 624 625 func (r *reader) Close() error { 626 if r.c == nil { 627 return nil 628 } 629 return r.c.Close() 630 } 631 632 func (r *reader) Attributes() *driver.ReaderAttributes { 633 return &r.attrs 634 } 635 636 func (r *reader) As(i interface{}) bool { 637 p, ok := i.(*io.Reader) 638 if !ok { 639 return false 640 } 641 *p = r.r 642 return true 643 } 644 645 func createTemp(path string) (*os.File, error) { 646 // Use a custom createTemp function rather than os.CreateTemp() as 647 // os.CreateTemp() sets the permissions of the tempfile to 0600, rather than 648 // 0666, making it inconsistent with the directories and attribute files. 649 try := 0 650 for { 651 // Append the current time with nanosecond precision and .tmp to the 652 // path. If the file already exists try again. Nanosecond changes enough 653 // between each iteration to make a conflict unlikely. Using the full 654 // time lowers the chance of a collision with a file using a similar 655 // pattern, but has undefined behavior after the year 2262. 656 name := path + "." + strconv.FormatInt(time.Now().UnixNano(), 16) + ".tmp" 657 f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666) 658 if os.IsExist(err) { 659 if try++; try < 10000 { 660 continue 661 } 662 return nil, &os.PathError{Op: "createtemp", Path: path + ".*.tmp", Err: os.ErrExist} 663 } 664 return f, err 665 } 666 } 667 668 // NewTypedWriter implements driver.NewTypedWriter. 669 func (b *bucket) NewTypedWriter(ctx context.Context, key string, contentType string, opts *driver.WriterOptions) (driver.Writer, error) { 670 path, err := b.path(key) 671 if err != nil { 672 return nil, err 673 } 674 if err := os.MkdirAll(filepath.Dir(path), os.FileMode(0777)); err != nil { 675 return nil, err 676 } 677 f, err := createTemp(path) 678 if err != nil { 679 return nil, err 680 } 681 if opts.BeforeWrite != nil { 682 if err := opts.BeforeWrite(func(i interface{}) bool { 683 p, ok := i.(**os.File) 684 if !ok { 685 return false 686 } 687 *p = f 688 return true 689 }); err != nil { 690 return nil, err 691 } 692 } 693 694 if b.opts.Metadata == MetadataDontWrite { 695 w := &writer{ 696 ctx: ctx, 697 File: f, 698 path: path, 699 } 700 return w, nil 701 } 702 703 var metadata map[string]string 704 if len(opts.Metadata) > 0 { 705 metadata = opts.Metadata 706 } 707 attrs := xattrs{ 708 CacheControl: opts.CacheControl, 709 ContentDisposition: opts.ContentDisposition, 710 ContentEncoding: opts.ContentEncoding, 711 ContentLanguage: opts.ContentLanguage, 712 ContentType: contentType, 713 Metadata: metadata, 714 } 715 w := &writerWithSidecar{ 716 ctx: ctx, 717 f: f, 718 path: path, 719 attrs: attrs, 720 contentMD5: opts.ContentMD5, 721 md5hash: md5.New(), 722 } 723 return w, nil 724 } 725 726 // writerWithSidecar implements the strategy of storing metadata in a distinct file. 727 type writerWithSidecar struct { 728 ctx context.Context 729 f *os.File 730 path string 731 attrs xattrs 732 contentMD5 []byte 733 // We compute the MD5 hash so that we can store it with the file attributes, 734 // not for verification. 735 md5hash hash.Hash 736 } 737 738 func (w *writerWithSidecar) Write(p []byte) (n int, err error) { 739 n, err = w.f.Write(p) 740 if err != nil { 741 // Don't hash the unwritten tail twice when writing is resumed. 742 w.md5hash.Write(p[:n]) 743 return n, err 744 } 745 if _, err := w.md5hash.Write(p); err != nil { 746 return n, err 747 } 748 return n, nil 749 } 750 751 func (w *writerWithSidecar) Close() error { 752 err := w.f.Close() 753 if err != nil { 754 return err 755 } 756 // Always delete the temp file. On success, it will have been renamed so 757 // the Remove will fail. 758 defer func() { 759 _ = os.Remove(w.f.Name()) 760 }() 761 762 // Check if the write was cancelled. 763 if err := w.ctx.Err(); err != nil { 764 return err 765 } 766 767 md5sum := w.md5hash.Sum(nil) 768 w.attrs.MD5 = md5sum 769 770 // Write the attributes file. 771 if err := setAttrs(w.path, w.attrs); err != nil { 772 return err 773 } 774 // Rename the temp file to path. 775 if err := os.Rename(w.f.Name(), w.path); err != nil { 776 _ = os.Remove(w.path + attrsExt) 777 return err 778 } 779 return nil 780 } 781 782 // writer is a file with a temporary name until closed. 783 // 784 // Embedding os.File allows the likes of io.Copy to use optimizations., 785 // which is why it is not folded into writerWithSidecar. 786 type writer struct { 787 *os.File 788 ctx context.Context 789 path string 790 } 791 792 func (w *writer) Close() error { 793 err := w.File.Close() 794 if err != nil { 795 return err 796 } 797 // Always delete the temp file. On success, it will have been renamed so 798 // the Remove will fail. 799 tempname := w.File.Name() 800 defer os.Remove(tempname) 801 802 // Check if the write was cancelled. 803 if err := w.ctx.Err(); err != nil { 804 return err 805 } 806 807 // Rename the temp file to path. 808 if err := os.Rename(tempname, w.path); err != nil { 809 return err 810 } 811 return nil 812 } 813 814 // Copy implements driver.Copy. 815 func (b *bucket) Copy(ctx context.Context, dstKey, srcKey string, opts *driver.CopyOptions) error { 816 // Note: we could use NewRangeReader here, but since we need to copy all of 817 // the metadata (from xa), it's more efficient to do it directly. 818 srcPath, _, xa, err := b.forKey(srcKey) 819 if err != nil { 820 return err 821 } 822 f, err := os.Open(srcPath) 823 if err != nil { 824 return err 825 } 826 defer f.Close() 827 828 // We'll write the copy using Writer, to avoid re-implementing making of a 829 // temp file, cleaning up after partial failures, etc. 830 wopts := driver.WriterOptions{ 831 CacheControl: xa.CacheControl, 832 ContentDisposition: xa.ContentDisposition, 833 ContentEncoding: xa.ContentEncoding, 834 ContentLanguage: xa.ContentLanguage, 835 Metadata: xa.Metadata, 836 BeforeWrite: opts.BeforeCopy, 837 } 838 // Create a cancelable context so we can cancel the write if there are 839 // problems. 840 writeCtx, cancel := context.WithCancel(ctx) 841 defer cancel() 842 w, err := b.NewTypedWriter(writeCtx, dstKey, xa.ContentType, &wopts) 843 if err != nil { 844 return err 845 } 846 _, err = io.Copy(w, f) 847 if err != nil { 848 cancel() // cancel before Close cancels the write 849 w.Close() 850 return err 851 } 852 return w.Close() 853 } 854 855 // Delete implements driver.Delete. 856 func (b *bucket) Delete(ctx context.Context, key string) error { 857 path, err := b.path(key) 858 if err != nil { 859 return err 860 } 861 err = os.Remove(path) 862 if err != nil { 863 return err 864 } 865 if err = os.Remove(path + attrsExt); err != nil && !os.IsNotExist(err) { 866 return err 867 } 868 return nil 869 } 870 871 // SignedURL implements driver.SignedURL 872 func (b *bucket) SignedURL(ctx context.Context, key string, opts *driver.SignedURLOptions) (string, error) { 873 if b.opts.URLSigner == nil { 874 return "", gcerr.New(gcerr.Unimplemented, nil, 1, "fileblob.SignedURL: bucket does not have an Options.URLSigner") 875 } 876 if opts.BeforeSign != nil { 877 if err := opts.BeforeSign(func(interface{}) bool { return false }); err != nil { 878 return "", err 879 } 880 } 881 surl, err := b.opts.URLSigner.URLFromKey(ctx, key, opts) 882 if err != nil { 883 return "", err 884 } 885 return surl.String(), nil 886 } 887 888 // URLSigner defines an interface for creating and verifying a signed URL for 889 // objects in a fileblob bucket. Signed URLs are typically used for granting 890 // access to an otherwise-protected resource without requiring further 891 // authentication, and callers should take care to restrict the creation of 892 // signed URLs as is appropriate for their application. 893 type URLSigner interface { 894 // URLFromKey defines how the bucket's object key will be turned 895 // into a signed URL. URLFromKey must be safe to call from multiple goroutines. 896 URLFromKey(ctx context.Context, key string, opts *driver.SignedURLOptions) (*url.URL, error) 897 898 // KeyFromURL must be able to validate a URL returned from URLFromKey. 899 // KeyFromURL must only return the object if if the URL is 900 // both unexpired and authentic. KeyFromURL must be safe to call from 901 // multiple goroutines. Implementations of KeyFromURL should not modify 902 // the URL argument. 903 KeyFromURL(ctx context.Context, surl *url.URL) (string, error) 904 } 905 906 // URLSignerHMAC signs URLs by adding the object key, expiration time, and a 907 // hash-based message authentication code (HMAC) into the query parameters. 908 // Values of URLSignerHMAC with the same secret key will accept URLs produced by 909 // others as valid. 910 type URLSignerHMAC struct { 911 baseURL *url.URL 912 secretKey []byte 913 } 914 915 // NewURLSignerHMAC creates a URLSignerHMAC. If the secret key is empty, 916 // then NewURLSignerHMAC panics. 917 func NewURLSignerHMAC(baseURL *url.URL, secretKey []byte) *URLSignerHMAC { 918 if len(secretKey) == 0 { 919 panic("creating URLSignerHMAC: secretKey is required") 920 } 921 uc := new(url.URL) 922 *uc = *baseURL 923 return &URLSignerHMAC{ 924 baseURL: uc, 925 secretKey: secretKey, 926 } 927 } 928 929 // URLFromKey creates a signed URL by copying the baseURL and appending the 930 // object key, expiry, and signature as a query params. 931 func (h *URLSignerHMAC) URLFromKey(ctx context.Context, key string, opts *driver.SignedURLOptions) (*url.URL, error) { 932 sURL := new(url.URL) 933 *sURL = *h.baseURL 934 935 q := sURL.Query() 936 q.Set("obj", key) 937 q.Set("expiry", strconv.FormatInt(time.Now().Add(opts.Expiry).Unix(), 10)) 938 q.Set("method", opts.Method) 939 if opts.ContentType != "" { 940 q.Set("contentType", opts.ContentType) 941 } 942 q.Set("signature", h.getMAC(q)) 943 sURL.RawQuery = q.Encode() 944 945 return sURL, nil 946 } 947 948 func (h *URLSignerHMAC) getMAC(q url.Values) string { 949 signedVals := url.Values{} 950 signedVals.Set("obj", q.Get("obj")) 951 signedVals.Set("expiry", q.Get("expiry")) 952 signedVals.Set("method", q.Get("method")) 953 if contentType := q.Get("contentType"); contentType != "" { 954 signedVals.Set("contentType", contentType) 955 } 956 msg := signedVals.Encode() 957 958 hsh := hmac.New(sha256.New, h.secretKey) 959 hsh.Write([]byte(msg)) 960 return base64.RawURLEncoding.EncodeToString(hsh.Sum(nil)) 961 } 962 963 // KeyFromURL checks expiry and signature, and returns the object key 964 // only if the signed URL is both authentic and unexpired. 965 func (h *URLSignerHMAC) KeyFromURL(ctx context.Context, sURL *url.URL) (string, error) { 966 q := sURL.Query() 967 968 exp, err := strconv.ParseInt(q.Get("expiry"), 10, 64) 969 if err != nil || time.Now().Unix() > exp { 970 return "", errors.New("retrieving blob key from URL: key cannot be retrieved") 971 } 972 973 if !h.checkMAC(q) { 974 return "", errors.New("retrieving blob key from URL: key cannot be retrieved") 975 } 976 return q.Get("obj"), nil 977 } 978 979 func (h *URLSignerHMAC) checkMAC(q url.Values) bool { 980 mac := q.Get("signature") 981 expected := h.getMAC(q) 982 // This compares the Base-64 encoded MACs 983 return hmac.Equal([]byte(mac), []byte(expected)) 984 }