github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/schema/schema.go (about) 1 /* 2 Copyright 2011 Google Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package schema manipulates Camlistore schema blobs. 18 // 19 // A schema blob is a JSON-encoded blob that describes other blobs. 20 // See documentation in Camlistore's doc/schema/ directory. 21 package schema 22 23 import ( 24 "bytes" 25 "crypto/rand" 26 "crypto/sha1" 27 "encoding/base64" 28 "encoding/json" 29 "errors" 30 "fmt" 31 "hash" 32 "io" 33 "os" 34 "reflect" 35 "strconv" 36 "strings" 37 "sync" 38 "time" 39 40 "camlistore.org/pkg/blob" 41 "camlistore.org/pkg/types" 42 "camlistore.org/third_party/github.com/camlistore/goexif/exif" 43 ) 44 45 // MaxSchemaBlobSize represents the upper bound for how large 46 // a schema blob may be. 47 const MaxSchemaBlobSize = 1 << 20 48 49 var sha1Type = reflect.TypeOf(sha1.New()) 50 51 var ( 52 ErrNoCamliVersion = errors.New("schema: no camliVersion key in map") 53 ) 54 55 var clockNow = time.Now 56 57 type StatHasher interface { 58 Lstat(fileName string) (os.FileInfo, error) 59 Hash(fileName string) (blob.Ref, error) 60 } 61 62 // File is the interface returned when opening a DirectoryEntry that 63 // is a regular file. 64 type File interface { 65 io.Closer 66 io.ReaderAt 67 io.Reader 68 Size() int64 69 } 70 71 // Directory is a read-only interface to a "directory" schema blob. 72 type Directory interface { 73 // Readdir reads the contents of the directory associated with dr 74 // and returns an array of up to n DirectoryEntries structures. 75 // Subsequent calls on the same file will yield further 76 // DirectoryEntries. 77 // If n > 0, Readdir returns at most n DirectoryEntry structures. In 78 // this case, if Readdir returns an empty slice, it will return 79 // a non-nil error explaining why. At the end of a directory, 80 // the error is os.EOF. 81 // If n <= 0, Readdir returns all the DirectoryEntries from the 82 // directory in a single slice. In this case, if Readdir succeeds 83 // (reads all the way to the end of the directory), it returns the 84 // slice and a nil os.Error. If it encounters an error before the 85 // end of the directory, Readdir returns the DirectoryEntry read 86 // until that point and a non-nil error. 87 Readdir(count int) ([]DirectoryEntry, error) 88 } 89 90 type Symlink interface { 91 // .. TODO 92 } 93 94 // DirectoryEntry is a read-only interface to an entry in a (static) 95 // directory. 96 type DirectoryEntry interface { 97 // CamliType returns the schema blob's "camliType" field. 98 // This may be "file", "directory", "symlink", or other more 99 // obscure types added in the future. 100 CamliType() string 101 102 FileName() string 103 BlobRef() blob.Ref 104 105 File() (File, error) // if camliType is "file" 106 Directory() (Directory, error) // if camliType is "directory" 107 Symlink() (Symlink, error) // if camliType is "symlink" 108 } 109 110 // dirEntry is the default implementation of DirectoryEntry 111 type dirEntry struct { 112 ss superset 113 fetcher blob.SeekFetcher 114 fr *FileReader // or nil if not a file 115 dr *DirReader // or nil if not a directory 116 } 117 118 func (de *dirEntry) CamliType() string { 119 return de.ss.Type 120 } 121 122 func (de *dirEntry) FileName() string { 123 return de.ss.FileNameString() 124 } 125 126 func (de *dirEntry) BlobRef() blob.Ref { 127 return de.ss.BlobRef 128 } 129 130 func (de *dirEntry) File() (File, error) { 131 if de.fr == nil { 132 if de.ss.Type != "file" { 133 return nil, fmt.Errorf("DirectoryEntry is camliType %q, not %q", de.ss.Type, "file") 134 } 135 fr, err := NewFileReader(de.fetcher, de.ss.BlobRef) 136 if err != nil { 137 return nil, err 138 } 139 de.fr = fr 140 } 141 return de.fr, nil 142 } 143 144 func (de *dirEntry) Directory() (Directory, error) { 145 if de.dr == nil { 146 if de.ss.Type != "directory" { 147 return nil, fmt.Errorf("DirectoryEntry is camliType %q, not %q", de.ss.Type, "directory") 148 } 149 dr, err := NewDirReader(de.fetcher, de.ss.BlobRef) 150 if err != nil { 151 return nil, err 152 } 153 de.dr = dr 154 } 155 return de.dr, nil 156 } 157 158 func (de *dirEntry) Symlink() (Symlink, error) { 159 return 0, errors.New("TODO: Symlink not implemented") 160 } 161 162 // newDirectoryEntry takes a superset and returns a DirectoryEntry if 163 // the Supserset is valid and represents an entry in a directory. It 164 // must by of type "file", "directory", or "symlink". 165 // TODO: "fifo", "socket", "char", "block", probably. later. 166 func newDirectoryEntry(fetcher blob.SeekFetcher, ss *superset) (DirectoryEntry, error) { 167 if ss == nil { 168 return nil, errors.New("ss was nil") 169 } 170 if !ss.BlobRef.Valid() { 171 return nil, errors.New("ss.BlobRef was invalid") 172 } 173 switch ss.Type { 174 case "file", "directory", "symlink": 175 // Okay 176 default: 177 return nil, fmt.Errorf("invalid DirectoryEntry camliType of %q", ss.Type) 178 } 179 de := &dirEntry{ss: *ss, fetcher: fetcher} // defensive copy 180 return de, nil 181 } 182 183 // NewDirectoryEntryFromBlobRef takes a BlobRef and returns a 184 // DirectoryEntry if the BlobRef contains a type "file", "directory" 185 // or "symlink". 186 // TODO: "fifo", "socket", "char", "block", probably. later. 187 func NewDirectoryEntryFromBlobRef(fetcher blob.SeekFetcher, blobRef blob.Ref) (DirectoryEntry, error) { 188 ss := new(superset) 189 err := ss.setFromBlobRef(fetcher, blobRef) 190 if err != nil { 191 return nil, fmt.Errorf("schema/filereader: can't fill superset: %v\n", err) 192 } 193 return newDirectoryEntry(fetcher, ss) 194 } 195 196 // superset represents the superset of common Camlistore JSON schema 197 // keys as a convenient json.Unmarshal target. 198 // TODO(bradfitz): unexport this type. Getting too gross. Move to schema.Blob 199 type superset struct { 200 // BlobRef isn't for a particular metadata blob field, but included 201 // for convenience. 202 BlobRef blob.Ref 203 204 Version int `json:"camliVersion"` 205 Type string `json:"camliType"` 206 207 Signer blob.Ref `json:"camliSigner"` 208 Sig string `json:"camliSig"` 209 210 ClaimType string `json:"claimType"` 211 ClaimDate types.Time3339 `json:"claimDate"` 212 213 Permanode blob.Ref `json:"permaNode"` 214 Attribute string `json:"attribute"` 215 Value string `json:"value"` 216 217 // FileName and FileNameBytes represent one of the two 218 // representations of file names in schema blobs. They should 219 // not be accessed directly. Use the FileNameString accessor 220 // instead, which also sanitizes malicious values. 221 FileName string `json:"fileName"` 222 FileNameBytes []interface{} `json:"fileNameBytes"` // TODO: needs custom UnmarshalJSON? 223 224 SymlinkTarget string `json:"symlinkTarget"` 225 SymlinkTargetBytes []interface{} `json:"symlinkTargetBytes"` // TODO: needs custom UnmarshalJSON? 226 227 UnixPermission string `json:"unixPermission"` 228 UnixOwnerId int `json:"unixOwnerId"` 229 UnixOwner string `json:"unixOwner"` 230 UnixGroupId int `json:"unixGroupId"` 231 UnixGroup string `json:"unixGroup"` 232 UnixMtime string `json:"unixMtime"` 233 UnixCtime string `json:"unixCtime"` 234 UnixAtime string `json:"unixAtime"` 235 236 // Parts are references to the data chunks of a regular file (or a "bytes" schema blob). 237 // See doc/schema/bytes.txt and doc/schema/files/file.txt. 238 Parts []*BytesPart `json:"parts"` 239 240 Entries blob.Ref `json:"entries"` // for directories, a blobref to a static-set 241 Members []blob.Ref `json:"members"` // for static sets (for directory static-sets: blobrefs to child dirs/files) 242 243 // Target is a "share" blob's target (the thing being shared) 244 // Or it is the object being deleted in a DeleteClaim claim. 245 Target blob.Ref `json:"target"` 246 // Transitive is a property of a "share" blob. 247 Transitive bool `json:"transitive"` 248 // AuthType is a "share" blob's authentication type that is required. 249 // Currently (2013-01-02) just "haveref" (if you know the share's blobref, 250 // you get access: the secret URL model) 251 AuthType string `json:"authType"` 252 Expires types.Time3339 `json:"expires"` // or zero for no expiration 253 } 254 255 func parseSuperset(r io.Reader) (*superset, error) { 256 var ss superset 257 if err := json.NewDecoder(io.LimitReader(r, 1<<20)).Decode(&ss); err != nil { 258 return nil, err 259 } 260 return &ss, nil 261 } 262 263 // BlobReader returns a new Blob from the provided Reader r, 264 // which should be the body of the provided blobref. 265 // Note: the hash checksum is not verified. 266 func BlobFromReader(ref blob.Ref, r io.Reader) (*Blob, error) { 267 if !ref.Valid() { 268 return nil, errors.New("schema.BlobFromReader: invalid blobref") 269 } 270 var buf bytes.Buffer 271 tee := io.TeeReader(r, &buf) 272 ss, err := parseSuperset(tee) 273 if err != nil { 274 return nil, err 275 } 276 var wb [16]byte 277 afterObj := 0 278 for { 279 n, err := tee.Read(wb[:]) 280 afterObj += n 281 for i := 0; i < n; i++ { 282 if !isASCIIWhite(wb[i]) { 283 return nil, fmt.Errorf("invalid bytes after JSON schema blob in %v", ref) 284 } 285 } 286 if afterObj > MaxSchemaBlobSize { 287 break 288 } 289 if err == io.EOF { 290 break 291 } 292 if err != nil { 293 return nil, err 294 } 295 } 296 json := buf.String() 297 if len(json) > MaxSchemaBlobSize { 298 return nil, fmt.Errorf("schema: metadata blob %v is over expected limit; size=%d", ref, len(json)) 299 } 300 return &Blob{ref, json, ss}, nil 301 } 302 303 func isASCIIWhite(b byte) bool { 304 switch b { 305 case ' ', '\t', '\r', '\n': 306 return true 307 } 308 return false 309 } 310 311 // BytesPart is the type representing one of the "parts" in a "file" 312 // or "bytes" JSON schema. 313 // 314 // See doc/schema/bytes.txt and doc/schema/files/file.txt. 315 type BytesPart struct { 316 // Size is the number of bytes that this part contributes to the overall segment. 317 Size uint64 `json:"size"` 318 319 // At most one of BlobRef or BytesRef must be non-zero 320 // (Valid), but it's illegal for both. 321 // If neither are set, this BytesPart represents Size zero bytes. 322 // BlobRef refers to raw bytes. BytesRef references a "bytes" schema blob. 323 BlobRef blob.Ref `json:"blobRef,omitempty"` 324 BytesRef blob.Ref `json:"bytesRef,omitempty"` 325 326 // Offset optionally specifies the offset into BlobRef to skip 327 // when reading Size bytes. 328 Offset uint64 `json:"offset,omitempty"` 329 } 330 331 // stringFromMixedArray joins a slice of either strings or float64 332 // values (as retrieved from JSON decoding) into a string. These are 333 // used for non-UTF8 filenames in "fileNameBytes" fields. The strings 334 // are UTF-8 segments and the float64s (actually uint8 values) are 335 // byte values. 336 func stringFromMixedArray(parts []interface{}) string { 337 var buf bytes.Buffer 338 for _, part := range parts { 339 if s, ok := part.(string); ok { 340 buf.WriteString(s) 341 continue 342 } 343 if num, ok := part.(float64); ok { 344 buf.WriteByte(byte(num)) 345 continue 346 } 347 } 348 return buf.String() 349 } 350 351 func (ss *superset) SumPartsSize() (size uint64) { 352 for _, part := range ss.Parts { 353 size += uint64(part.Size) 354 } 355 return size 356 } 357 358 func (ss *superset) SymlinkTargetString() string { 359 if ss.SymlinkTarget != "" { 360 return ss.SymlinkTarget 361 } 362 return stringFromMixedArray(ss.SymlinkTargetBytes) 363 } 364 365 // FileNameString returns the schema blob's base filename. 366 // 367 // If the fileName field of the blob accidentally or maliciously 368 // contains a slash, this function returns an empty string instead. 369 func (ss *superset) FileNameString() string { 370 v := ss.FileName 371 if v == "" { 372 v = stringFromMixedArray(ss.FileNameBytes) 373 } 374 if v != "" { 375 if strings.Index(v, "/") != -1 { 376 // Bogus schema blob; ignore. 377 return "" 378 } 379 if strings.Index(v, "\\") != -1 { 380 // Bogus schema blob; ignore. 381 return "" 382 } 383 } 384 return v 385 } 386 387 func (ss *superset) HasFilename(name string) bool { 388 return ss.FileNameString() == name 389 } 390 391 func (b *Blob) FileMode() os.FileMode { 392 // TODO: move this to a different type, off *Blob 393 return b.ss.FileMode() 394 } 395 396 func (ss *superset) FileMode() os.FileMode { 397 var mode os.FileMode 398 m64, err := strconv.ParseUint(ss.UnixPermission, 8, 64) 399 if err == nil { 400 mode = mode | os.FileMode(m64) 401 } 402 403 // TODO: add other types (block, char, etc) 404 switch ss.Type { 405 case "directory": 406 mode = mode | os.ModeDir 407 case "file": 408 // No extra bit. 409 case "symlink": 410 mode = mode | os.ModeSymlink 411 } 412 return mode 413 } 414 415 // MapUid returns the most appropriate mapping from this file's owner 416 // to the local machine's owner, trying first a match by name, 417 // followed by just mapping the number through directly. 418 func (b *Blob) MapUid() int { return b.ss.MapUid() } 419 420 // MapGid returns the most appropriate mapping from this file's group 421 // to the local machine's group, trying first a match by name, 422 // followed by just mapping the number through directly. 423 func (b *Blob) MapGid() int { return b.ss.MapGid() } 424 425 func (ss *superset) MapUid() int { 426 if ss.UnixOwner != "" { 427 uid, ok := getUidFromName(ss.UnixOwner) 428 if ok { 429 return uid 430 } 431 } 432 return ss.UnixOwnerId // TODO: will be 0 if unset, which isn't ideal 433 } 434 435 func (ss *superset) MapGid() int { 436 if ss.UnixGroup != "" { 437 gid, ok := getGidFromName(ss.UnixGroup) 438 if ok { 439 return gid 440 } 441 } 442 return ss.UnixGroupId // TODO: will be 0 if unset, which isn't ideal 443 } 444 445 func (ss *superset) ModTime() time.Time { 446 if ss.UnixMtime == "" { 447 return time.Time{} 448 } 449 t, err := time.Parse(time.RFC3339, ss.UnixMtime) 450 if err != nil { 451 return time.Time{} 452 } 453 return t 454 } 455 456 var DefaultStatHasher = &defaultStatHasher{} 457 458 type defaultStatHasher struct{} 459 460 func (d *defaultStatHasher) Lstat(fileName string) (os.FileInfo, error) { 461 return os.Lstat(fileName) 462 } 463 464 func (d *defaultStatHasher) Hash(fileName string) (blob.Ref, error) { 465 s1 := sha1.New() 466 file, err := os.Open(fileName) 467 if err != nil { 468 return blob.Ref{}, err 469 } 470 defer file.Close() 471 _, err = io.Copy(s1, file) 472 if err != nil { 473 return blob.Ref{}, err 474 } 475 return blob.RefFromHash(s1), nil 476 } 477 478 type StaticSet struct { 479 l sync.Mutex 480 refs []blob.Ref 481 } 482 483 func (ss *StaticSet) Add(ref blob.Ref) { 484 ss.l.Lock() 485 defer ss.l.Unlock() 486 ss.refs = append(ss.refs, ref) 487 } 488 489 func base(version int, ctype string) *Builder { 490 return &Builder{map[string]interface{}{ 491 "camliVersion": version, 492 "camliType": ctype, 493 }} 494 } 495 496 // NewUnsignedPermanode returns a new random permanode, not yet signed. 497 func NewUnsignedPermanode() *Builder { 498 bb := base(1, "permanode") 499 chars := make([]byte, 20) 500 _, err := io.ReadFull(rand.Reader, chars) 501 if err != nil { 502 panic("error reading random bytes: " + err.Error()) 503 } 504 bb.m["random"] = base64.StdEncoding.EncodeToString(chars) 505 return bb 506 } 507 508 // NewPlannedPermanode returns a permanode with a fixed key. Like 509 // NewUnsignedPermanode, this builder is also not yet signed. Callers of 510 // NewPlannedPermanode must sign the map with a fixed claimDate and 511 // GPG date to create consistent JSON encodings of the Map (its 512 // blobref), between runs. 513 func NewPlannedPermanode(key string) *Builder { 514 bb := base(1, "permanode") 515 bb.m["key"] = key 516 return bb 517 } 518 519 // NewHashPlannedPermanode returns a planned permanode with the sum 520 // of the hash, prefixed with "sha1-", as the key. 521 func NewHashPlannedPermanode(h hash.Hash) *Builder { 522 if reflect.TypeOf(h) != sha1Type { 523 panic("Hash not supported. Only sha1 for now.") 524 } 525 return NewPlannedPermanode(fmt.Sprintf("sha1-%x", h.Sum(nil))) 526 } 527 528 // Map returns a Camli map of camliType "static-set" 529 // TODO: delete this method 530 func (ss *StaticSet) Blob() *Blob { 531 bb := base(1, "static-set") 532 ss.l.Lock() 533 defer ss.l.Unlock() 534 535 members := make([]string, 0, len(ss.refs)) 536 if ss.refs != nil { 537 for _, ref := range ss.refs { 538 members = append(members, ref.String()) 539 } 540 } 541 bb.m["members"] = members 542 return bb.Blob() 543 } 544 545 // JSON returns the map m encoded as JSON in its 546 // recommended canonical form. The canonical form is readable with newlines and indentation, 547 // and always starts with the header bytes: 548 // 549 // {"camliVersion": 550 // 551 func mapJSON(m map[string]interface{}) (string, error) { 552 version, hasVersion := m["camliVersion"] 553 if !hasVersion { 554 return "", ErrNoCamliVersion 555 } 556 delete(m, "camliVersion") 557 jsonBytes, err := json.MarshalIndent(m, "", " ") 558 if err != nil { 559 return "", err 560 } 561 m["camliVersion"] = version 562 var buf bytes.Buffer 563 fmt.Fprintf(&buf, "{\"camliVersion\": %v,\n", version) 564 buf.Write(jsonBytes[2:]) 565 return buf.String(), nil 566 } 567 568 // NewFileMap returns a new builder of a type "file" schema for the provided fileName. 569 // The chunk parts of the file are not populated. 570 func NewFileMap(fileName string) *Builder { 571 return newCommonFilenameMap(fileName).SetType("file") 572 } 573 574 // NewDirMap returns a new builder of a type "directory" schema for the provided fileName. 575 func NewDirMap(fileName string) *Builder { 576 return newCommonFilenameMap(fileName).SetType("directory") 577 } 578 579 func newCommonFilenameMap(fileName string) *Builder { 580 bb := base(1, "" /* no type yet */) 581 if fileName != "" { 582 bb.SetFileName(fileName) 583 } 584 return bb 585 } 586 587 var populateSchemaStat []func(schemaMap map[string]interface{}, fi os.FileInfo) 588 589 func NewCommonFileMap(fileName string, fi os.FileInfo) *Builder { 590 bb := newCommonFilenameMap(fileName) 591 // Common elements (from file-common.txt) 592 if fi.Mode()&os.ModeSymlink == 0 { 593 bb.m["unixPermission"] = fmt.Sprintf("0%o", fi.Mode().Perm()) 594 } 595 596 // OS-specific population; defined in schema_posix.go, etc. (not on App Engine) 597 for _, f := range populateSchemaStat { 598 f(bb.m, fi) 599 } 600 601 if mtime := fi.ModTime(); !mtime.IsZero() { 602 bb.m["unixMtime"] = RFC3339FromTime(mtime) 603 } 604 return bb 605 } 606 607 // PopulateParts sets the "parts" field of the blob with the provided 608 // parts. The sum of the sizes of parts must match the provided size 609 // or an error is returned. Also, each BytesPart may only contain either 610 // a BytesPart or a BlobRef, but not both. 611 func (bb *Builder) PopulateParts(size int64, parts []BytesPart) error { 612 return populateParts(bb.m, size, parts) 613 } 614 615 func populateParts(m map[string]interface{}, size int64, parts []BytesPart) error { 616 sumSize := int64(0) 617 mparts := make([]map[string]interface{}, len(parts)) 618 for idx, part := range parts { 619 mpart := make(map[string]interface{}) 620 mparts[idx] = mpart 621 switch { 622 case part.BlobRef.Valid() && part.BytesRef.Valid(): 623 return errors.New("schema: part contains both BlobRef and BytesRef") 624 case part.BlobRef.Valid(): 625 mpart["blobRef"] = part.BlobRef.String() 626 case part.BytesRef.Valid(): 627 mpart["bytesRef"] = part.BytesRef.String() 628 default: 629 return errors.New("schema: part must contain either a BlobRef or BytesRef") 630 } 631 mpart["size"] = part.Size 632 sumSize += int64(part.Size) 633 if part.Offset != 0 { 634 mpart["offset"] = part.Offset 635 } 636 } 637 if sumSize != size { 638 return fmt.Errorf("schema: declared size %d doesn't match sum of parts size %d", size, sumSize) 639 } 640 m["parts"] = mparts 641 return nil 642 } 643 644 func newBytes() *Builder { 645 return base(1, "bytes") 646 } 647 648 // ClaimType is one of the valid "claimType" fields in a "claim" schema blob. See doc/schema/claims/. 649 type ClaimType string 650 651 const ( 652 SetAttributeClaim ClaimType = "set-attribute" 653 AddAttributeClaim ClaimType = "add-attribute" 654 DelAttributeClaim ClaimType = "del-attribute" 655 ShareClaim ClaimType = "share" 656 // DeleteClaim deletes a permanode or another claim. 657 // A delete claim can itself be deleted, and so on. 658 DeleteClaim ClaimType = "delete" 659 ) 660 661 // claimParam is used to populate a claim map when building a new claim 662 type claimParam struct { 663 claimType ClaimType 664 665 // Params specific to *Attribute claims: 666 permanode blob.Ref // modified permanode 667 attribute string // required 668 value string // optional if Type == DelAttributeClaim 669 670 // Params specific to ShareClaim claims: 671 authType string 672 transitive bool 673 shareExpires time.Time // Zero means no expiration 674 675 // Params specific to ShareClaim and DeleteClaim claims. 676 target blob.Ref 677 } 678 679 func NewClaim(claims ...*claimParam) *Builder { 680 bb := base(1, "claim") 681 bb.SetClaimDate(clockNow()) 682 if len(claims) == 1 { 683 cp := claims[0] 684 populateClaimMap(bb.m, cp) 685 return bb 686 } 687 var claimList []interface{} 688 for _, cp := range claims { 689 m := map[string]interface{}{} 690 populateClaimMap(m, cp) 691 claimList = append(claimList, m) 692 } 693 bb.m["claimType"] = "multi" 694 bb.m["claims"] = claimList 695 return bb 696 } 697 698 func populateClaimMap(m map[string]interface{}, cp *claimParam) { 699 m["claimType"] = string(cp.claimType) 700 switch cp.claimType { 701 case ShareClaim: 702 m["authType"] = cp.authType 703 m["target"] = cp.target.String() 704 m["transitive"] = cp.transitive 705 case DeleteClaim: 706 m["target"] = cp.target.String() 707 default: 708 m["permaNode"] = cp.permanode.String() 709 m["attribute"] = cp.attribute 710 if !(cp.claimType == DelAttributeClaim && cp.value == "") { 711 m["value"] = cp.value 712 } 713 } 714 } 715 716 // NewShareRef creates a *Builder for a "share" claim. 717 func NewShareRef(authType string, target blob.Ref, transitive bool) *Builder { 718 return NewClaim(&claimParam{ 719 claimType: ShareClaim, 720 authType: authType, 721 target: target, 722 transitive: transitive, 723 }) 724 } 725 726 func NewSetAttributeClaim(permaNode blob.Ref, attr, value string) *Builder { 727 return NewClaim(&claimParam{ 728 permanode: permaNode, 729 claimType: SetAttributeClaim, 730 attribute: attr, 731 value: value, 732 }) 733 } 734 735 func NewAddAttributeClaim(permaNode blob.Ref, attr, value string) *Builder { 736 return NewClaim(&claimParam{ 737 permanode: permaNode, 738 claimType: AddAttributeClaim, 739 attribute: attr, 740 value: value, 741 }) 742 } 743 744 // NewDelAttributeClaim creates a new claim to remove value from the 745 // values set for the attribute attr of permaNode. If value is empty then 746 // all the values for attribute are cleared. 747 func NewDelAttributeClaim(permaNode blob.Ref, attr, value string) *Builder { 748 return NewClaim(&claimParam{ 749 permanode: permaNode, 750 claimType: DelAttributeClaim, 751 attribute: attr, 752 value: value, 753 }) 754 } 755 756 // NewDeleteClaim creates a new claim to delete a target claim or permanode. 757 func NewDeleteClaim(target blob.Ref) *Builder { 758 return NewClaim(&claimParam{ 759 target: target, 760 claimType: DeleteClaim, 761 }) 762 } 763 764 // ShareHaveRef is the auth type specifying that if you "have the 765 // reference" (know the blobref to the haveref share blob), then you 766 // have access to the referenced object from that share blob. 767 // This is the "send a link to a friend" access model. 768 const ShareHaveRef = "haveref" 769 770 // RFC3339FromTime returns an RFC3339-formatted time in UTC. 771 // Fractional seconds are only included if the time has fractional 772 // seconds. 773 func RFC3339FromTime(t time.Time) string { 774 if t.UnixNano()%1e9 == 0 { 775 return t.UTC().Format(time.RFC3339) 776 } 777 return t.UTC().Format(time.RFC3339Nano) 778 } 779 780 var bytesCamliVersion = []byte("camliVersion") 781 782 // LikelySchemaBlob returns quickly whether buf likely contains (or is 783 // the prefix of) a schema blob. 784 func LikelySchemaBlob(buf []byte) bool { 785 if len(buf) == 0 || buf[0] != '{' { 786 return false 787 } 788 return bytes.Contains(buf, bytesCamliVersion) 789 } 790 791 // findSize checks if v is an *os.File or if it has 792 // a Size() int64 method, to find its size. 793 // It returns 0, false otherwise. 794 func findSize(v interface{}) (size int64, ok bool) { 795 if fi, ok := v.(*os.File); ok { 796 v, _ = fi.Stat() 797 } 798 if sz, ok := v.(interface { 799 Size() int64 800 }); ok { 801 return sz.Size(), true 802 } 803 // For bytes.Reader, strings.Reader, etc: 804 if li, ok := v.(interface { 805 Len() int 806 }); ok { 807 ln := int64(li.Len()) // unread portion, typically 808 // If it's also a seeker, remove add any seek offset: 809 if sk, ok := v.(io.Seeker); ok { 810 if cur, err := sk.Seek(0, 1); err == nil { 811 ln += cur 812 } 813 } 814 return ln, true 815 } 816 return 0, false 817 } 818 819 // FileTime returns the best guess of the file's creation time (or modtime). 820 // If the file doesn't have its own metadata indication the creation time (such as in EXIF), 821 // FileTime uses the modification time from the file system. 822 // It there was a valid EXIF but an error while trying to get a date from it, 823 // it logs the error and tries the other methods. 824 func FileTime(f io.ReaderAt) (time.Time, error) { 825 var ct time.Time 826 defaultTime := func() (time.Time, error) { 827 if osf, ok := f.(*os.File); ok { 828 fi, err := osf.Stat() 829 if err != nil { 830 return ct, fmt.Errorf("Failed to find a modtime: lstat: %v", err) 831 } 832 return fi.ModTime(), nil 833 } 834 return ct, errors.New("All methods failed to find a creation time or modtime.") 835 } 836 837 size, ok := findSize(f) 838 if !ok { 839 size = 256 << 10 // enough to get the EXIF 840 } 841 r := io.NewSectionReader(f, 0, size) 842 ex, err := exif.Decode(r) 843 if err != nil { 844 return defaultTime() 845 } 846 ct, err = ex.DateTime() 847 if err != nil { 848 return defaultTime() 849 } 850 return ct, nil 851 }