From d4da3cf4aa96dcc2fb6293209199f0c20eb13a1e Mon Sep 17 00:00:00 2001 From: Dirk McCormick Date: Wed, 29 Mar 2023 15:54:39 +0200 Subject: [PATCH] feat: use go-libipfs lib (instead of copying to extern) --- cmd/booster-http/blocks.go | 6 +- cmd/booster-http/run.go | 2 +- cmd/booster-http/server.go | 2 +- extern/go-libipfs/LICENSE.md | 229 ----- extern/go-libipfs/blocks/blocks.go | 82 -- extern/go-libipfs/files/README.md | 22 - extern/go-libipfs/files/file.go | 96 -- extern/go-libipfs/files/filewriter.go | 59 -- extern/go-libipfs/files/filewriter_unix.go | 19 - extern/go-libipfs/files/filewriter_windows.go | 45 - extern/go-libipfs/files/filter.go | 49 - extern/go-libipfs/files/is_hidden.go | 17 - extern/go-libipfs/files/is_hidden_windows.go | 32 - extern/go-libipfs/files/linkfile.go | 42 - extern/go-libipfs/files/multifilereader.go | 152 --- extern/go-libipfs/files/multipartfile.go | 230 ----- extern/go-libipfs/files/readerfile.go | 81 -- extern/go-libipfs/files/serialfile.go | 168 ---- extern/go-libipfs/files/slicedirectory.go | 97 -- extern/go-libipfs/files/tarwriter.go | 137 --- extern/go-libipfs/files/util.go | 25 - extern/go-libipfs/files/walk.go | 27 - extern/go-libipfs/files/webfile.go | 89 -- extern/go-libipfs/gateway/README.md | 34 - extern/go-libipfs/gateway/assets/README.md | 27 - extern/go-libipfs/gateway/assets/assets.go | 203 ---- extern/go-libipfs/gateway/assets/build.sh | 14 - .../go-libipfs/gateway/assets/dag-index.html | 67 -- .../gateway/assets/directory-index.html | 99 -- .../go-libipfs/gateway/assets/knownIcons.txt | 65 -- .../gateway/assets/src/dag-index.html | 66 -- .../gateway/assets/src/directory-index.html | 98 -- .../go-libipfs/gateway/assets/src/icons.css | 403 -------- .../go-libipfs/gateway/assets/src/style.css | 212 ---- extern/go-libipfs/gateway/assets/test/go.mod | 3 - extern/go-libipfs/gateway/assets/test/main.go | 156 --- extern/go-libipfs/gateway/gateway.go | 119 --- extern/go-libipfs/gateway/handler.go | 929 ------------------ extern/go-libipfs/gateway/handler_block.go | 51 - extern/go-libipfs/gateway/handler_car.go | 99 -- extern/go-libipfs/gateway/handler_codec.go | 269 ----- .../go-libipfs/gateway/handler_ipns_record.go | 90 -- extern/go-libipfs/gateway/handler_tar.go | 95 -- extern/go-libipfs/gateway/handler_unixfs.go | 44 - .../gateway/handler_unixfs__redirects.go | 287 ------ .../go-libipfs/gateway/handler_unixfs_dir.go | 211 ---- .../go-libipfs/gateway/handler_unixfs_file.go | 105 -- extern/go-libipfs/gateway/hostname.go | 592 ----------- extern/go-libipfs/gateway/lazyseek.go | 60 -- .../go-libipfs/gateway/testdata/fixtures.car | Bin 1688 -> 0 bytes go.mod | 24 +- 51 files changed, 17 insertions(+), 6113 deletions(-) delete mode 100644 extern/go-libipfs/LICENSE.md delete mode 100644 extern/go-libipfs/blocks/blocks.go delete mode 100644 extern/go-libipfs/files/README.md delete mode 100644 extern/go-libipfs/files/file.go delete mode 100644 extern/go-libipfs/files/filewriter.go delete mode 100644 extern/go-libipfs/files/filewriter_unix.go delete mode 100644 extern/go-libipfs/files/filewriter_windows.go delete mode 100644 extern/go-libipfs/files/filter.go delete mode 100644 extern/go-libipfs/files/is_hidden.go delete mode 100644 extern/go-libipfs/files/is_hidden_windows.go delete mode 100644 extern/go-libipfs/files/linkfile.go delete mode 100644 extern/go-libipfs/files/multifilereader.go delete mode 100644 extern/go-libipfs/files/multipartfile.go delete mode 100644 extern/go-libipfs/files/readerfile.go delete mode 100644 extern/go-libipfs/files/serialfile.go delete mode 100644 extern/go-libipfs/files/slicedirectory.go delete mode 100644 extern/go-libipfs/files/tarwriter.go delete mode 100644 extern/go-libipfs/files/util.go delete mode 100644 extern/go-libipfs/files/walk.go delete mode 100644 extern/go-libipfs/files/webfile.go delete mode 100644 extern/go-libipfs/gateway/README.md delete mode 100644 extern/go-libipfs/gateway/assets/README.md delete mode 100644 extern/go-libipfs/gateway/assets/assets.go delete mode 100755 extern/go-libipfs/gateway/assets/build.sh delete mode 100644 extern/go-libipfs/gateway/assets/dag-index.html delete mode 100644 extern/go-libipfs/gateway/assets/directory-index.html delete mode 100644 extern/go-libipfs/gateway/assets/knownIcons.txt delete mode 100644 extern/go-libipfs/gateway/assets/src/dag-index.html delete mode 100644 extern/go-libipfs/gateway/assets/src/directory-index.html delete mode 100644 extern/go-libipfs/gateway/assets/src/icons.css delete mode 100644 extern/go-libipfs/gateway/assets/src/style.css delete mode 100644 extern/go-libipfs/gateway/assets/test/go.mod delete mode 100644 extern/go-libipfs/gateway/assets/test/main.go delete mode 100644 extern/go-libipfs/gateway/gateway.go delete mode 100644 extern/go-libipfs/gateway/handler.go delete mode 100644 extern/go-libipfs/gateway/handler_block.go delete mode 100644 extern/go-libipfs/gateway/handler_car.go delete mode 100644 extern/go-libipfs/gateway/handler_codec.go delete mode 100644 extern/go-libipfs/gateway/handler_ipns_record.go delete mode 100644 extern/go-libipfs/gateway/handler_tar.go delete mode 100644 extern/go-libipfs/gateway/handler_unixfs.go delete mode 100644 extern/go-libipfs/gateway/handler_unixfs__redirects.go delete mode 100644 extern/go-libipfs/gateway/handler_unixfs_dir.go delete mode 100644 extern/go-libipfs/gateway/handler_unixfs_file.go delete mode 100644 extern/go-libipfs/gateway/hostname.go delete mode 100644 extern/go-libipfs/gateway/lazyseek.go delete mode 100644 extern/go-libipfs/gateway/testdata/fixtures.car diff --git a/cmd/booster-http/blocks.go b/cmd/booster-http/blocks.go index 5a3b920b4..0275ddf77 100644 --- a/cmd/booster-http/blocks.go +++ b/cmd/booster-http/blocks.go @@ -7,22 +7,22 @@ import ( "context" "errors" "fmt" - ufile "github.com/ipfs/go-unixfs/file" gopath "path" - "github.com/filecoin-project/boost/extern/go-libipfs/blocks" - "github.com/filecoin-project/boost/extern/go-libipfs/files" "github.com/ipfs/go-blockservice" "github.com/ipfs/go-cid" bsfetcher "github.com/ipfs/go-fetcher/impl/blockservice" blockstore "github.com/ipfs/go-ipfs-blockstore" format "github.com/ipfs/go-ipld-format" + "github.com/ipfs/go-libipfs/blocks" + "github.com/ipfs/go-libipfs/files" "github.com/ipfs/go-merkledag" "github.com/ipfs/go-namesys" "github.com/ipfs/go-namesys/resolve" ipfspath "github.com/ipfs/go-path" "github.com/ipfs/go-path/resolver" "github.com/ipfs/go-unixfs" + ufile "github.com/ipfs/go-unixfs/file" uio "github.com/ipfs/go-unixfs/io" "github.com/ipfs/go-unixfsnode" iface "github.com/ipfs/interface-go-ipfs-core" diff --git a/cmd/booster-http/run.go b/cmd/booster-http/run.go index 4bf3c202f..3f3af001f 100644 --- a/cmd/booster-http/run.go +++ b/cmd/booster-http/run.go @@ -83,7 +83,7 @@ var runCmd = &cli.Command{ }, &cli.StringFlag{ Name: "api-filter-endpoint", - Usage: "the endpoint to use for fetching a remote retrieval configuration for bitswap requests", + Usage: "the endpoint to use for fetching a remote retrieval configuration for requests", }, &cli.StringFlag{ Name: "api-filter-auth", diff --git a/cmd/booster-http/server.go b/cmd/booster-http/server.go index a4fc1e08e..a8765edf2 100644 --- a/cmd/booster-http/server.go +++ b/cmd/booster-http/server.go @@ -15,7 +15,6 @@ import ( "github.com/fatih/color" "github.com/filecoin-project/boost-gfm/piecestore" "github.com/filecoin-project/boost-gfm/retrievalmarket" - "github.com/filecoin-project/boost/extern/go-libipfs/gateway" "github.com/filecoin-project/boost/metrics" "github.com/filecoin-project/boostd-data/shared/tracing" "github.com/filecoin-project/dagstore/mount" @@ -26,6 +25,7 @@ import ( "github.com/ipfs/go-datastore" blockstore "github.com/ipfs/go-ipfs-blockstore" offline "github.com/ipfs/go-ipfs-exchange-offline" + "github.com/ipfs/go-libipfs/gateway" "go.opencensus.io/stats" ) diff --git a/extern/go-libipfs/LICENSE.md b/extern/go-libipfs/LICENSE.md deleted file mode 100644 index 2fa16a153..000000000 --- a/extern/go-libipfs/LICENSE.md +++ /dev/null @@ -1,229 +0,0 @@ -The contents of this repository are Copyright (c) corresponding authors and -contributors, licensed under the `Permissive License Stack` meaning either of: - -- Apache-2.0 Software License: https://www.apache.org/licenses/LICENSE-2.0 - ([...4tr2kfsq](https://dweb.link/ipfs/bafkreiankqxazcae4onkp436wag2lj3ccso4nawxqkkfckd6cg4tr2kfsq)) - -- MIT Software License: https://opensource.org/licenses/MIT - ([...vljevcba](https://dweb.link/ipfs/bafkreiepofszg4gfe2gzuhojmksgemsub2h4uy2gewdnr35kswvljevcba)) - -You may not use the contents of this repository except in compliance -with one of the listed Licenses. For an extended clarification of the -intent behind the choice of Licensing please refer to -https://protocol.ai/blog/announcing-the-permissive-license-stack/ - -Unless required by applicable law or agreed to in writing, software -distributed under the terms listed in this notice is distributed on -an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, -either express or implied. See each License for the specific language -governing permissions and limitations under that License. - - -`SPDX-License-Identifier: Apache-2.0 OR MIT` - -Verbatim copies of both licenses are included below: - -
Apache-2.0 Software License - -``` - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS -``` -
- -
MIT Software License - -``` -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -``` -
diff --git a/extern/go-libipfs/blocks/blocks.go b/extern/go-libipfs/blocks/blocks.go deleted file mode 100644 index 3d3894b3f..000000000 --- a/extern/go-libipfs/blocks/blocks.go +++ /dev/null @@ -1,82 +0,0 @@ -// Package blocks contains the lowest level of IPLD data structures. -// A block is raw data accompanied by a CID. The CID contains the multihash -// corresponding to the block. -package blocks - -import ( - "errors" - "fmt" - - cid "github.com/ipfs/go-cid" - u "github.com/ipfs/go-ipfs-util" - mh "github.com/multiformats/go-multihash" -) - -// ErrWrongHash is returned when the Cid of a block is not the expected -// according to the contents. It is currently used only when debugging. -var ErrWrongHash = errors.New("data did not match given hash") - -// Block provides abstraction for blocks implementations. -type Block interface { - RawData() []byte - Cid() cid.Cid - String() string - Loggable() map[string]interface{} -} - -// A BasicBlock is a singular block of data in ipfs. It implements the Block -// interface. -type BasicBlock struct { - cid cid.Cid - data []byte -} - -// NewBlock creates a Block object from opaque data. It will hash the data. -func NewBlock(data []byte) *BasicBlock { - // TODO: fix assumptions - return &BasicBlock{data: data, cid: cid.NewCidV0(u.Hash(data))} -} - -// NewBlockWithCid creates a new block when the hash of the data -// is already known, this is used to save time in situations where -// we are able to be confident that the data is correct. -func NewBlockWithCid(data []byte, c cid.Cid) (*BasicBlock, error) { - if u.Debug { - chkc, err := c.Prefix().Sum(data) - if err != nil { - return nil, err - } - - if !chkc.Equals(c) { - return nil, ErrWrongHash - } - } - return &BasicBlock{data: data, cid: c}, nil -} - -// Multihash returns the hash contained in the block CID. -func (b *BasicBlock) Multihash() mh.Multihash { - return b.cid.Hash() -} - -// RawData returns the block raw contents as a byte slice. -func (b *BasicBlock) RawData() []byte { - return b.data -} - -// Cid returns the content identifier of the block. -func (b *BasicBlock) Cid() cid.Cid { - return b.cid -} - -// String provides a human-readable representation of the block CID. -func (b *BasicBlock) String() string { - return fmt.Sprintf("[Block %s]", b.Cid()) -} - -// Loggable returns a go-log loggable item. -func (b *BasicBlock) Loggable() map[string]interface{} { - return map[string]interface{}{ - "block": b.Cid().String(), - } -} diff --git a/extern/go-libipfs/files/README.md b/extern/go-libipfs/files/README.md deleted file mode 100644 index b5720c5ef..000000000 --- a/extern/go-libipfs/files/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# go-libipfs/files - -> File interfaces and utils used in GO implementations of [IPFS](https://ipfs.tech) - -## Documentation - -https://pkg.go.dev/github.com/ipfs/go-libipfs/files - -## Contribute - -Feel free to join in. All welcome. Open an [issue](https://github.com/ipfs/go-libipfs/issues)! - -This repository falls under the IPFS [Code of Conduct](https://github.com/ipfs/community/blob/master/code-of-conduct.md). - -### Want to hack on IPFS? - -[![](https://cdn.rawgit.com/jbenet/contribute-ipfs-gif/master/img/contribute.gif)](https://github.com/ipfs/community/blob/master/CONTRIBUTING.md) - -## License - -MIT - diff --git a/extern/go-libipfs/files/file.go b/extern/go-libipfs/files/file.go deleted file mode 100644 index 7ac1fc98a..000000000 --- a/extern/go-libipfs/files/file.go +++ /dev/null @@ -1,96 +0,0 @@ -package files - -import ( - "errors" - "io" - "os" -) - -var ( - ErrNotDirectory = errors.New("file isn't a directory") - ErrNotReader = errors.New("file isn't a regular file") - - ErrNotSupported = errors.New("operation not supported") -) - -// Node is a common interface for files, directories and other special files -type Node interface { - io.Closer - - // Size returns size of this file (if this file is a directory, total size of - // all files stored in the tree should be returned). Some implementations may - // choose not to implement this - Size() (int64, error) -} - -// Node represents a regular Unix file -type File interface { - Node - - io.Reader - io.Seeker -} - -// DirEntry exposes information about a directory entry -type DirEntry interface { - // Name returns base name of this entry, which is the base name of referenced - // file - Name() string - - // Node returns the file referenced by this DirEntry - Node() Node -} - -// DirIterator is a iterator over directory entries. -// See Directory.Entries for more -type DirIterator interface { - // DirEntry holds information about current directory entry. - // Note that after creating new iterator you MUST call Next() at least once - // before accessing these methods. Calling these methods without prior calls - // to Next() and after Next() returned false may result in undefined behavior - DirEntry - - // Next advances iterator to the next file. - Next() bool - - // Err may return an error after previous call to Next() returned `false`. - // If previous call to Next() returned `true`, Err() is guaranteed to - // return nil - Err() error -} - -// Directory is a special file which can link to any number of files. -type Directory interface { - Node - - // Entries returns a stateful iterator over directory entries. The iterator - // may consume the Directory state so it must be called only once (this - // applies specifically to the multipartIterator). - // - // Example usage: - // - // it := dir.Entries() - // for it.Next() { - // name := it.Name() - // file := it.Node() - // [...] - // } - // if it.Err() != nil { - // return err - // } - // - // Note that you can't store the result of it.Node() and use it after - // advancing the iterator - Entries() DirIterator -} - -// FileInfo exposes information on files in local filesystem -type FileInfo interface { - Node - - // AbsPath returns full real file path. - AbsPath() string - - // Stat returns os.Stat of this file, may be nil for some files - Stat() os.FileInfo -} diff --git a/extern/go-libipfs/files/filewriter.go b/extern/go-libipfs/files/filewriter.go deleted file mode 100644 index bf4bcf649..000000000 --- a/extern/go-libipfs/files/filewriter.go +++ /dev/null @@ -1,59 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "io" - "os" - "path/filepath" -) - -var ErrInvalidDirectoryEntry = errors.New("invalid directory entry name") -var ErrPathExistsOverwrite = errors.New("path already exists and overwriting is not allowed") - -// WriteTo writes the given node to the local filesystem at fpath. -func WriteTo(nd Node, fpath string) error { - if _, err := os.Lstat(fpath); err == nil { - return ErrPathExistsOverwrite - } else if !os.IsNotExist(err) { - return err - } - switch nd := nd.(type) { - case *Symlink: - return os.Symlink(nd.Target, fpath) - case File: - f, err := createNewFile(fpath) - defer f.Close() - if err != nil { - return err - } - _, err = io.Copy(f, nd) - if err != nil { - return err - } - return nil - case Directory: - err := os.Mkdir(fpath, 0777) - if err != nil { - return err - } - - entries := nd.Entries() - for entries.Next() { - entryName := entries.Name() - if entryName == "" || - entryName == "." || - entryName == ".." || - !isValidFilename(entryName) { - return ErrInvalidDirectoryEntry - } - child := filepath.Join(fpath, entryName) - if err := WriteTo(entries.Node(), child); err != nil { - return err - } - } - return entries.Err() - default: - return fmt.Errorf("file type %T at %q is not supported", nd, fpath) - } -} diff --git a/extern/go-libipfs/files/filewriter_unix.go b/extern/go-libipfs/files/filewriter_unix.go deleted file mode 100644 index 98d040018..000000000 --- a/extern/go-libipfs/files/filewriter_unix.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build darwin || linux || netbsd || openbsd || freebsd || dragonfly - -package files - -import ( - "os" - "strings" - "syscall" -) - -var invalidChars = `/` + "\x00" - -func isValidFilename(filename string) bool { - return !strings.ContainsAny(filename, invalidChars) -} - -func createNewFile(path string) (*os.File, error) { - return os.OpenFile(path, os.O_EXCL|os.O_CREATE|os.O_WRONLY|syscall.O_NOFOLLOW, 0666) -} diff --git a/extern/go-libipfs/files/filewriter_windows.go b/extern/go-libipfs/files/filewriter_windows.go deleted file mode 100644 index a5d626199..000000000 --- a/extern/go-libipfs/files/filewriter_windows.go +++ /dev/null @@ -1,45 +0,0 @@ -//go:build windows - -package files - -import ( - "os" - "strings" -) - -var invalidChars = `<>:"/\|?*` + "\x00" - -var reservedNames = map[string]struct{}{ - "CON": {}, - "PRN": {}, - "AUX": {}, - "NUL": {}, - "COM1": {}, - "COM2": {}, - "COM3": {}, - "COM4": {}, - "COM5": {}, - "COM6": {}, - "COM7": {}, - "COM8": {}, - "COM9": {}, - "LPT1": {}, - "LPT2": {}, - "LPT3": {}, - "LPT4": {}, - "LPT5": {}, - "LPT6": {}, - "LPT7": {}, - "LPT8": {}, - "LPT9": {}, -} - -func isValidFilename(filename string) bool { - _, isReservedName := reservedNames[filename] - return !strings.ContainsAny(filename, invalidChars) && - !isReservedName -} - -func createNewFile(path string) (*os.File, error) { - return os.OpenFile(path, os.O_EXCL|os.O_CREATE|os.O_WRONLY, 0666) -} diff --git a/extern/go-libipfs/files/filter.go b/extern/go-libipfs/files/filter.go deleted file mode 100644 index 6b90f1f34..000000000 --- a/extern/go-libipfs/files/filter.go +++ /dev/null @@ -1,49 +0,0 @@ -package files - -import ( - "os" - - ignore "github.com/crackcomm/go-gitignore" -) - -// Filter represents a set of rules for determining if a file should be included or excluded. -// A rule follows the syntax for patterns used in .gitgnore files for specifying untracked files. -// Examples: -// foo.txt -// *.app -// bar/ -// **/baz -// fizz/** -type Filter struct { - // IncludeHidden - Include hidden files - IncludeHidden bool - // Rules - File filter rules - Rules *ignore.GitIgnore -} - -// NewFilter creates a new file filter from a .gitignore file and/or a list of ignore rules. -// An ignoreFile is a path to a file with .gitignore-style patterns to exclude, one per line -// rules is an array of strings representing .gitignore-style patterns -// For reference on ignore rule syntax, see https://git-scm.com/docs/gitignore -func NewFilter(ignoreFile string, rules []string, includeHidden bool) (*Filter, error) { - var ignoreRules *ignore.GitIgnore - var err error - if ignoreFile == "" { - ignoreRules, err = ignore.CompileIgnoreLines(rules...) - } else { - ignoreRules, err = ignore.CompileIgnoreFileAndLines(ignoreFile, rules...) - } - if err != nil { - return nil, err - } - return &Filter{IncludeHidden: includeHidden, Rules: ignoreRules}, nil -} - -// ShouldExclude takes an os.FileInfo object and applies rules to determine if its target should be excluded. -func (filter *Filter) ShouldExclude(fileInfo os.FileInfo) (result bool) { - path := fileInfo.Name() - if !filter.IncludeHidden && isHidden(fileInfo) { - return true - } - return filter.Rules.MatchesPath(path) -} diff --git a/extern/go-libipfs/files/is_hidden.go b/extern/go-libipfs/files/is_hidden.go deleted file mode 100644 index 9842ca232..000000000 --- a/extern/go-libipfs/files/is_hidden.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !windows - -package files - -import ( - "os" -) - -func isHidden(fi os.FileInfo) bool { - fName := fi.Name() - switch fName { - case "", ".", "..": - return false - default: - return fName[0] == '.' - } -} diff --git a/extern/go-libipfs/files/is_hidden_windows.go b/extern/go-libipfs/files/is_hidden_windows.go deleted file mode 100644 index 9a0703863..000000000 --- a/extern/go-libipfs/files/is_hidden_windows.go +++ /dev/null @@ -1,32 +0,0 @@ -//go:build windows - -package files - -import ( - "os" - - windows "golang.org/x/sys/windows" -) - -func isHidden(fi os.FileInfo) bool { - fName := fi.Name() - switch fName { - case "", ".", "..": - return false - } - - if fName[0] == '.' { - return true - } - - sys := fi.Sys() - if sys == nil { - return false - } - wi, ok := sys.(*windows.Win32FileAttributeData) - if !ok { - return false - } - - return wi.FileAttributes&windows.FILE_ATTRIBUTE_HIDDEN != 0 -} diff --git a/extern/go-libipfs/files/linkfile.go b/extern/go-libipfs/files/linkfile.go deleted file mode 100644 index 526998652..000000000 --- a/extern/go-libipfs/files/linkfile.go +++ /dev/null @@ -1,42 +0,0 @@ -package files - -import ( - "os" - "strings" -) - -type Symlink struct { - Target string - - stat os.FileInfo - reader strings.Reader -} - -func NewLinkFile(target string, stat os.FileInfo) File { - lf := &Symlink{Target: target, stat: stat} - lf.reader.Reset(lf.Target) - return lf -} - -func (lf *Symlink) Close() error { - return nil -} - -func (lf *Symlink) Read(b []byte) (int, error) { - return lf.reader.Read(b) -} - -func (lf *Symlink) Seek(offset int64, whence int) (int64, error) { - return lf.reader.Seek(offset, whence) -} - -func (lf *Symlink) Size() (int64, error) { - return lf.reader.Size(), nil -} - -func ToSymlink(n Node) *Symlink { - l, _ := n.(*Symlink) - return l -} - -var _ File = &Symlink{} diff --git a/extern/go-libipfs/files/multifilereader.go b/extern/go-libipfs/files/multifilereader.go deleted file mode 100644 index f6f225a38..000000000 --- a/extern/go-libipfs/files/multifilereader.go +++ /dev/null @@ -1,152 +0,0 @@ -package files - -import ( - "bytes" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "net/url" - "path" - "sync" -) - -// MultiFileReader reads from a `commands.Node` (which can be a directory of files -// or a regular file) as HTTP multipart encoded data. -type MultiFileReader struct { - io.Reader - - // directory stack for NextFile - files []DirIterator - path []string - - currentFile Node - buf bytes.Buffer - mpWriter *multipart.Writer - closed bool - mutex *sync.Mutex - - // if true, the content disposition will be "form-data" - // if false, the content disposition will be "attachment" - form bool -} - -// NewMultiFileReader constructs a MultiFileReader. `file` can be any `commands.Directory`. -// If `form` is set to true, the Content-Disposition will be "form-data". -// Otherwise, it will be "attachment". -func NewMultiFileReader(file Directory, form bool) *MultiFileReader { - it := file.Entries() - - mfr := &MultiFileReader{ - files: []DirIterator{it}, - path: []string{""}, - form: form, - mutex: &sync.Mutex{}, - } - mfr.mpWriter = multipart.NewWriter(&mfr.buf) - - return mfr -} - -func (mfr *MultiFileReader) Read(buf []byte) (written int, err error) { - mfr.mutex.Lock() - defer mfr.mutex.Unlock() - - // if we are closed and the buffer is flushed, end reading - if mfr.closed && mfr.buf.Len() == 0 { - return 0, io.EOF - } - - // if the current file isn't set, advance to the next file - if mfr.currentFile == nil { - var entry DirEntry - - for entry == nil { - if len(mfr.files) == 0 { - mfr.mpWriter.Close() - mfr.closed = true - return mfr.buf.Read(buf) - } - - if !mfr.files[len(mfr.files)-1].Next() { - if mfr.files[len(mfr.files)-1].Err() != nil { - return 0, mfr.files[len(mfr.files)-1].Err() - } - mfr.files = mfr.files[:len(mfr.files)-1] - mfr.path = mfr.path[:len(mfr.path)-1] - continue - } - - entry = mfr.files[len(mfr.files)-1] - } - - // handle starting a new file part - if !mfr.closed { - - mfr.currentFile = entry.Node() - - // write the boundary and headers - header := make(textproto.MIMEHeader) - filename := url.QueryEscape(path.Join(path.Join(mfr.path...), entry.Name())) - dispositionPrefix := "attachment" - if mfr.form { - dispositionPrefix = "form-data; name=\"file\"" - } - - header.Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"", dispositionPrefix, filename)) - - var contentType string - - switch f := entry.Node().(type) { - case *Symlink: - contentType = "application/symlink" - case Directory: - newIt := f.Entries() - mfr.files = append(mfr.files, newIt) - mfr.path = append(mfr.path, entry.Name()) - contentType = "application/x-directory" - case File: - // otherwise, use the file as a reader to read its contents - contentType = "application/octet-stream" - default: - return 0, ErrNotSupported - } - - header.Set("Content-Type", contentType) - if rf, ok := entry.Node().(FileInfo); ok { - header.Set("abspath", rf.AbsPath()) - } - - _, err := mfr.mpWriter.CreatePart(header) - if err != nil { - return 0, err - } - } - } - - // if the buffer has something in it, read from it - if mfr.buf.Len() > 0 { - return mfr.buf.Read(buf) - } - - // otherwise, read from file data - switch f := mfr.currentFile.(type) { - case File: - written, err = f.Read(buf) - if err != io.EOF { - return written, err - } - } - - if err := mfr.currentFile.Close(); err != nil { - return written, err - } - - mfr.currentFile = nil - return written, nil -} - -// Boundary returns the boundary string to be used to separate files in the multipart data -func (mfr *MultiFileReader) Boundary() string { - return mfr.mpWriter.Boundary() -} diff --git a/extern/go-libipfs/files/multipartfile.go b/extern/go-libipfs/files/multipartfile.go deleted file mode 100644 index 6052a4108..000000000 --- a/extern/go-libipfs/files/multipartfile.go +++ /dev/null @@ -1,230 +0,0 @@ -package files - -import ( - "io" - "mime" - "mime/multipart" - "net/url" - "path" - "strings" -) - -const ( - multipartFormdataType = "multipart/form-data" - - applicationDirectory = "application/x-directory" - applicationSymlink = "application/symlink" - - contentTypeHeader = "Content-Type" -) - -type multipartDirectory struct { - path string - walker *multipartWalker - - // part is the part describing the directory. It's nil when implicit. - part *multipart.Part -} - -type multipartWalker struct { - part *multipart.Part - reader *multipart.Reader -} - -func (m *multipartWalker) consumePart() { - m.part = nil -} - -func (m *multipartWalker) getPart() (*multipart.Part, error) { - if m.part != nil { - return m.part, nil - } - if m.reader == nil { - return nil, io.EOF - } - - var err error - m.part, err = m.reader.NextPart() - if err == io.EOF { - m.reader = nil - } - return m.part, err -} - -// NewFileFromPartReader creates a Directory from a multipart reader. -func NewFileFromPartReader(reader *multipart.Reader, mediatype string) (Directory, error) { - switch mediatype { - case applicationDirectory, multipartFormdataType: - default: - return nil, ErrNotDirectory - } - - return &multipartDirectory{ - path: "/", - walker: &multipartWalker{ - reader: reader, - }, - }, nil -} - -func (w *multipartWalker) nextFile() (Node, error) { - part, err := w.getPart() - if err != nil { - return nil, err - } - w.consumePart() - - contentType := part.Header.Get(contentTypeHeader) - if contentType != "" { - var err error - contentType, _, err = mime.ParseMediaType(contentType) - if err != nil { - return nil, err - } - } - - switch contentType { - case multipartFormdataType, applicationDirectory: - return &multipartDirectory{ - part: part, - path: fileName(part), - walker: w, - }, nil - case applicationSymlink: - out, err := io.ReadAll(part) - if err != nil { - return nil, err - } - - return NewLinkFile(string(out), nil), nil - default: - return &ReaderFile{ - reader: part, - abspath: part.Header.Get("abspath"), - }, nil - } -} - -// fileName returns a normalized filename from a part. -func fileName(part *multipart.Part) string { - v := part.Header.Get("Content-Disposition") - _, params, err := mime.ParseMediaType(v) - if err != nil { - return "" - } - filename := params["filename"] - if escaped, err := url.QueryUnescape(filename); err == nil { - filename = escaped - } // if there is a unescape error, just treat the name as unescaped - - return path.Clean("/" + filename) -} - -// dirName appends a slash to the end of the filename, if not present. -// expects a _cleaned_ path. -func dirName(filename string) string { - if !strings.HasSuffix(filename, "/") { - filename += "/" - } - return filename -} - -// isChild checks if child is a child of parent directory. -// expects a _cleaned_ path. -func isChild(child, parent string) bool { - return strings.HasPrefix(child, dirName(parent)) -} - -// makeRelative makes the child path relative to the parent path. -// expects a _cleaned_ path. -func makeRelative(child, parent string) string { - return strings.TrimPrefix(child, dirName(parent)) -} - -type multipartIterator struct { - f *multipartDirectory - - curFile Node - curName string - err error -} - -func (it *multipartIterator) Name() string { - return it.curName -} - -func (it *multipartIterator) Node() Node { - return it.curFile -} - -func (it *multipartIterator) Next() bool { - if it.f.walker.reader == nil || it.err != nil { - return false - } - var part *multipart.Part - for { - part, it.err = it.f.walker.getPart() - if it.err != nil { - return false - } - - name := fileName(part) - - // Is the file in a different directory? - if !isChild(name, it.f.path) { - return false - } - - // Have we already entered this directory? - if it.curName != "" && isChild(name, path.Join(it.f.path, it.curName)) { - it.f.walker.consumePart() - continue - } - - // Make the path relative to the current directory. - name = makeRelative(name, it.f.path) - - // Check if we need to create a fake directory (more than one - // path component). - if idx := strings.IndexByte(name, '/'); idx >= 0 { - it.curName = name[:idx] - it.curFile = &multipartDirectory{ - path: path.Join(it.f.path, it.curName), - walker: it.f.walker, - } - return true - } - it.curName = name - - // Finally, advance to the next file. - it.curFile, it.err = it.f.walker.nextFile() - - return it.err == nil - } -} - -func (it *multipartIterator) Err() error { - // We use EOF to signal that this iterator is done. That way, we don't - // need to check every time `Next` is called. - if it.err == io.EOF { - return nil - } - return it.err -} - -func (f *multipartDirectory) Entries() DirIterator { - return &multipartIterator{f: f} -} - -func (f *multipartDirectory) Close() error { - if f.part != nil { - return f.part.Close() - } - return nil -} - -func (f *multipartDirectory) Size() (int64, error) { - return 0, ErrNotSupported -} - -var _ Directory = &multipartDirectory{} diff --git a/extern/go-libipfs/files/readerfile.go b/extern/go-libipfs/files/readerfile.go deleted file mode 100644 index a03dae23f..000000000 --- a/extern/go-libipfs/files/readerfile.go +++ /dev/null @@ -1,81 +0,0 @@ -package files - -import ( - "bytes" - "io" - "os" - "path/filepath" -) - -// ReaderFile is a implementation of File created from an `io.Reader`. -// ReaderFiles are never directories, and can be read from and closed. -type ReaderFile struct { - abspath string - reader io.ReadCloser - stat os.FileInfo - - fsize int64 -} - -func NewBytesFile(b []byte) File { - return &ReaderFile{"", NewReaderFile(bytes.NewReader(b)), nil, int64(len(b))} -} - -func NewReaderFile(reader io.Reader) File { - return NewReaderStatFile(reader, nil) -} - -func NewReaderStatFile(reader io.Reader, stat os.FileInfo) File { - rc, ok := reader.(io.ReadCloser) - if !ok { - rc = io.NopCloser(reader) - } - - return &ReaderFile{"", rc, stat, -1} -} - -func NewReaderPathFile(path string, reader io.ReadCloser, stat os.FileInfo) (*ReaderFile, error) { - abspath, err := filepath.Abs(path) - if err != nil { - return nil, err - } - - return &ReaderFile{abspath, reader, stat, -1}, nil -} - -func (f *ReaderFile) AbsPath() string { - return f.abspath -} - -func (f *ReaderFile) Read(p []byte) (int, error) { - return f.reader.Read(p) -} - -func (f *ReaderFile) Close() error { - return f.reader.Close() -} - -func (f *ReaderFile) Stat() os.FileInfo { - return f.stat -} - -func (f *ReaderFile) Size() (int64, error) { - if f.stat == nil { - if f.fsize >= 0 { - return f.fsize, nil - } - return 0, ErrNotSupported - } - return f.stat.Size(), nil -} - -func (f *ReaderFile) Seek(offset int64, whence int) (int64, error) { - if s, ok := f.reader.(io.Seeker); ok { - return s.Seek(offset, whence) - } - - return 0, ErrNotSupported -} - -var _ File = &ReaderFile{} -var _ FileInfo = &ReaderFile{} diff --git a/extern/go-libipfs/files/serialfile.go b/extern/go-libipfs/files/serialfile.go deleted file mode 100644 index 176038cde..000000000 --- a/extern/go-libipfs/files/serialfile.go +++ /dev/null @@ -1,168 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "io/fs" - "os" - "path/filepath" -) - -// serialFile implements Node, and reads from a path on the OS filesystem. -// No more than one file will be opened at a time. -type serialFile struct { - path string - files []os.FileInfo - stat os.FileInfo - filter *Filter -} - -type serialIterator struct { - files []os.FileInfo - path string - filter *Filter - - curName string - curFile Node - - err error -} - -// NewSerialFile takes a filepath, a bool specifying if hidden files should be included, -// and a fileInfo and returns a Node representing file, directory or special file. -func NewSerialFile(path string, includeHidden bool, stat os.FileInfo) (Node, error) { - filter, err := NewFilter("", nil, includeHidden) - if err != nil { - return nil, err - } - return NewSerialFileWithFilter(path, filter, stat) -} - -// NewSerialFileWith takes a filepath, a filter for determining which files should be -// operated upon if the filepath is a directory, and a fileInfo and returns a -// Node representing file, directory or special file. -func NewSerialFileWithFilter(path string, filter *Filter, stat os.FileInfo) (Node, error) { - switch mode := stat.Mode(); { - case mode.IsRegular(): - file, err := os.Open(path) - if err != nil { - return nil, err - } - return NewReaderPathFile(path, file, stat) - case mode.IsDir(): - // for directories, stat all of the contents first, so we know what files to - // open when Entries() is called - entries, err := os.ReadDir(path) - if err != nil { - return nil, err - } - contents := make([]fs.FileInfo, 0, len(entries)) - for _, entry := range entries { - content, err := entry.Info() - if err != nil { - return nil, err - } - contents = append(contents, content) - } - return &serialFile{path, contents, stat, filter}, nil - case mode&os.ModeSymlink != 0: - target, err := os.Readlink(path) - if err != nil { - return nil, err - } - return NewLinkFile(target, stat), nil - default: - return nil, fmt.Errorf("unrecognized file type for %s: %s", path, mode.String()) - } -} - -func (it *serialIterator) Name() string { - return it.curName -} - -func (it *serialIterator) Node() Node { - return it.curFile -} - -func (it *serialIterator) Next() bool { - // if there aren't any files left in the root directory, we're done - if len(it.files) == 0 { - return false - } - - stat := it.files[0] - it.files = it.files[1:] - for it.filter.ShouldExclude(stat) { - if len(it.files) == 0 { - return false - } - - stat = it.files[0] - it.files = it.files[1:] - } - - // open the next file - filePath := filepath.ToSlash(filepath.Join(it.path, stat.Name())) - - // recursively call the constructor on the next file - // if it's a regular file, we will open it as a ReaderFile - // if it's a directory, files in it will be opened serially - sf, err := NewSerialFileWithFilter(filePath, it.filter, stat) - if err != nil { - it.err = err - return false - } - - it.curName = stat.Name() - it.curFile = sf - return true -} - -func (it *serialIterator) Err() error { - return it.err -} - -func (f *serialFile) Entries() DirIterator { - return &serialIterator{ - path: f.path, - files: f.files, - filter: f.filter, - } -} - -func (f *serialFile) Close() error { - return nil -} - -func (f *serialFile) Stat() os.FileInfo { - return f.stat -} - -func (f *serialFile) Size() (int64, error) { - if !f.stat.IsDir() { - //something went terribly, terribly wrong - return 0, errors.New("serialFile is not a directory") - } - - var du int64 - err := filepath.Walk(f.path, func(p string, fi os.FileInfo, err error) error { - if err != nil || fi == nil { - return err - } - - if f.filter.ShouldExclude(fi) { - if fi.Mode().IsDir() { - return filepath.SkipDir - } - } else if fi.Mode().IsRegular() { - du += fi.Size() - } - - return nil - }) - - return du, err -} - -var _ Directory = &serialFile{} -var _ DirIterator = &serialIterator{} diff --git a/extern/go-libipfs/files/slicedirectory.go b/extern/go-libipfs/files/slicedirectory.go deleted file mode 100644 index d11656261..000000000 --- a/extern/go-libipfs/files/slicedirectory.go +++ /dev/null @@ -1,97 +0,0 @@ -package files - -import "sort" - -type fileEntry struct { - name string - file Node -} - -func (e fileEntry) Name() string { - return e.name -} - -func (e fileEntry) Node() Node { - return e.file -} - -func FileEntry(name string, file Node) DirEntry { - return fileEntry{ - name: name, - file: file, - } -} - -type sliceIterator struct { - files []DirEntry - n int -} - -func (it *sliceIterator) Name() string { - return it.files[it.n].Name() -} - -func (it *sliceIterator) Node() Node { - return it.files[it.n].Node() -} - -func (it *sliceIterator) Next() bool { - it.n++ - return it.n < len(it.files) -} - -func (it *sliceIterator) Err() error { - return nil -} - -// SliceFile implements Node, and provides simple directory handling. -// It contains children files, and is created from a `[]Node`. -// SliceFiles are always directories, and can't be read from or closed. -type SliceFile struct { - files []DirEntry -} - -func NewMapDirectory(f map[string]Node) Directory { - ents := make([]DirEntry, 0, len(f)) - for name, nd := range f { - ents = append(ents, FileEntry(name, nd)) - } - sort.Slice(ents, func(i, j int) bool { - return ents[i].Name() < ents[j].Name() - }) - - return NewSliceDirectory(ents) -} - -func NewSliceDirectory(files []DirEntry) Directory { - return &SliceFile{files} -} - -func (f *SliceFile) Entries() DirIterator { - return &sliceIterator{files: f.files, n: -1} -} - -func (f *SliceFile) Close() error { - return nil -} - -func (f *SliceFile) Length() int { - return len(f.files) -} - -func (f *SliceFile) Size() (int64, error) { - var size int64 - - for _, file := range f.files { - s, err := file.Node().Size() - if err != nil { - return 0, err - } - size += s - } - - return size, nil -} - -var _ Directory = &SliceFile{} -var _ DirEntry = fileEntry{} diff --git a/extern/go-libipfs/files/tarwriter.go b/extern/go-libipfs/files/tarwriter.go deleted file mode 100644 index cecbcae42..000000000 --- a/extern/go-libipfs/files/tarwriter.go +++ /dev/null @@ -1,137 +0,0 @@ -package files - -import ( - "archive/tar" - "errors" - "fmt" - "io" - "path" - "strings" - "time" -) - -var ( - ErrUnixFSPathOutsideRoot = errors.New("relative UnixFS paths outside the root are now allowed, use CAR instead") -) - -type TarWriter struct { - TarW *tar.Writer - baseDirSet bool - baseDir string -} - -// NewTarWriter wraps given io.Writer into a new tar writer -func NewTarWriter(w io.Writer) (*TarWriter, error) { - return &TarWriter{ - TarW: tar.NewWriter(w), - }, nil -} - -func (w *TarWriter) writeDir(f Directory, fpath string) error { - if err := writeDirHeader(w.TarW, fpath); err != nil { - return err - } - - it := f.Entries() - for it.Next() { - if err := w.WriteFile(it.Node(), path.Join(fpath, it.Name())); err != nil { - return err - } - } - return it.Err() -} - -func (w *TarWriter) writeFile(f File, fpath string) error { - size, err := f.Size() - if err != nil { - return err - } - - if err := writeFileHeader(w.TarW, fpath, uint64(size)); err != nil { - return err - } - - if _, err := io.Copy(w.TarW, f); err != nil { - return err - } - w.TarW.Flush() - return nil -} - -func validateTarFilePath(baseDir, fpath string) bool { - // Ensure the filepath has no ".", "..", etc within the known root directory. - fpath = path.Clean(fpath) - - // If we have a non-empty baseDir, check if the filepath starts with baseDir. - // If not, we can exclude it immediately. For 'ipfs get' and for the gateway, - // the baseDir would be '{cid}.tar'. - if baseDir != "" && !strings.HasPrefix(path.Clean(fpath), baseDir) { - return false - } - - // Otherwise, check if the path starts with '..' which would make it fall - // outside the root path. This works since the path has already been cleaned. - if strings.HasPrefix(fpath, "..") { - return false - } - - return true -} - -// WriteNode adds a node to the archive. -func (w *TarWriter) WriteFile(nd Node, fpath string) error { - if !w.baseDirSet { - w.baseDirSet = true // Use a variable for this as baseDir may be an empty string. - w.baseDir = fpath - } - - if !validateTarFilePath(w.baseDir, fpath) { - return ErrUnixFSPathOutsideRoot - } - - switch nd := nd.(type) { - case *Symlink: - return writeSymlinkHeader(w.TarW, nd.Target, fpath) - case File: - return w.writeFile(nd, fpath) - case Directory: - return w.writeDir(nd, fpath) - default: - return fmt.Errorf("file type %T is not supported", nd) - } -} - -// Close closes the tar writer. -func (w *TarWriter) Close() error { - return w.TarW.Close() -} - -func writeDirHeader(w *tar.Writer, fpath string) error { - return w.WriteHeader(&tar.Header{ - Name: fpath, - Typeflag: tar.TypeDir, - Mode: 0777, - ModTime: time.Now().Truncate(time.Second), - // TODO: set mode, dates, etc. when added to unixFS - }) -} - -func writeFileHeader(w *tar.Writer, fpath string, size uint64) error { - return w.WriteHeader(&tar.Header{ - Name: fpath, - Size: int64(size), - Typeflag: tar.TypeReg, - Mode: 0644, - ModTime: time.Now().Truncate(time.Second), - // TODO: set mode, dates, etc. when added to unixFS - }) -} - -func writeSymlinkHeader(w *tar.Writer, target, fpath string) error { - return w.WriteHeader(&tar.Header{ - Name: fpath, - Linkname: target, - Mode: 0777, - Typeflag: tar.TypeSymlink, - }) -} diff --git a/extern/go-libipfs/files/util.go b/extern/go-libipfs/files/util.go deleted file mode 100644 index e727e7ae6..000000000 --- a/extern/go-libipfs/files/util.go +++ /dev/null @@ -1,25 +0,0 @@ -package files - -// ToFile is an alias for n.(File). If the file isn't a regular file, nil value -// will be returned -func ToFile(n Node) File { - f, _ := n.(File) - return f -} - -// ToDir is an alias for n.(Directory). If the file isn't directory, a nil value -// will be returned -func ToDir(n Node) Directory { - d, _ := n.(Directory) - return d -} - -// FileFromEntry calls ToFile on Node in the given entry -func FileFromEntry(e DirEntry) File { - return ToFile(e.Node()) -} - -// DirFromEntry calls ToDir on Node in the given entry -func DirFromEntry(e DirEntry) Directory { - return ToDir(e.Node()) -} diff --git a/extern/go-libipfs/files/walk.go b/extern/go-libipfs/files/walk.go deleted file mode 100644 index f23e7e47f..000000000 --- a/extern/go-libipfs/files/walk.go +++ /dev/null @@ -1,27 +0,0 @@ -package files - -import ( - "path/filepath" -) - -// Walk walks a file tree, like `os.Walk`. -func Walk(nd Node, cb func(fpath string, nd Node) error) error { - var helper func(string, Node) error - helper = func(path string, nd Node) error { - if err := cb(path, nd); err != nil { - return err - } - dir, ok := nd.(Directory) - if !ok { - return nil - } - iter := dir.Entries() - for iter.Next() { - if err := helper(filepath.Join(path, iter.Name()), iter.Node()); err != nil { - return err - } - } - return iter.Err() - } - return helper("", nd) -} diff --git a/extern/go-libipfs/files/webfile.go b/extern/go-libipfs/files/webfile.go deleted file mode 100644 index 594b81c82..000000000 --- a/extern/go-libipfs/files/webfile.go +++ /dev/null @@ -1,89 +0,0 @@ -package files - -import ( - "errors" - "fmt" - "io" - "net/http" - "net/url" - "os" -) - -// WebFile is an implementation of File which reads it -// from a Web URL (http). A GET request will be performed -// against the source when calling Read(). -type WebFile struct { - body io.ReadCloser - url *url.URL - contentLength int64 -} - -// NewWebFile creates a WebFile with the given URL, which -// will be used to perform the GET request on Read(). -func NewWebFile(url *url.URL) *WebFile { - return &WebFile{ - url: url, - } -} - -func (wf *WebFile) start() error { - if wf.body == nil { - s := wf.url.String() - resp, err := http.Get(s) - if err != nil { - return err - } - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return fmt.Errorf("got non-2XX status code %d: %s", resp.StatusCode, s) - } - wf.body = resp.Body - wf.contentLength = resp.ContentLength - } - return nil -} - -// Read reads the File from it's web location. On the first -// call to Read, a GET request will be performed against the -// WebFile's URL, using Go's default HTTP client. Any further -// reads will keep reading from the HTTP Request body. -func (wf *WebFile) Read(b []byte) (int, error) { - if err := wf.start(); err != nil { - return 0, err - } - return wf.body.Read(b) -} - -// Close closes the WebFile (or the request body). -func (wf *WebFile) Close() error { - if wf.body == nil { - return nil - } - return wf.body.Close() -} - -// TODO: implement -func (wf *WebFile) Seek(offset int64, whence int) (int64, error) { - return 0, ErrNotSupported -} - -func (wf *WebFile) Size() (int64, error) { - if err := wf.start(); err != nil { - return 0, err - } - if wf.contentLength < 0 { - return -1, errors.New("Content-Length hearer was not set") - } - - return wf.contentLength, nil -} - -func (wf *WebFile) AbsPath() string { - return wf.url.String() -} - -func (wf *WebFile) Stat() os.FileInfo { - return nil -} - -var _ File = &WebFile{} -var _ FileInfo = &WebFile{} diff --git a/extern/go-libipfs/gateway/README.md b/extern/go-libipfs/gateway/README.md deleted file mode 100644 index 6c7a7cf47..000000000 --- a/extern/go-libipfs/gateway/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# IPFS Gateway - -> A reference implementation of HTTP Gateway Specifications. - -## Documentation - -* Go Documentation: https://pkg.go.dev/github.com/ipfs/go-libipfs/gateway -* Gateway Specification: https://github.com/ipfs/specs/tree/main/http-gateways#readme -* Types of HTTP Gateways: https://docs.ipfs.tech/how-to/address-ipfs-on-web/#http-gateways -## Example - -```go -// Initialize your headers and apply the default headers. -headers := map[string][]string{} -gateway.AddAccessControlHeaders(headers) - -conf := gateway.Config{ - Headers: headers, -} - -// Initialize a NodeAPI interface for both an online and offline versions. -// The offline version should not make any network request for missing content. -ipfs := ... - -// Create http mux and setup path gateway handler. -mux := http.NewServeMux() -gwHandler := gateway.NewHandler(conf, ipfs) -mux.Handle("/ipfs/", gwHandler) -mux.Handle("/ipns/", gwHandler) - -// Start the server on :8080 and voilá! You have a basic IPFS gateway running -// in http://localhost:8080. -_ = http.ListenAndServe(":8080", mux) -``` diff --git a/extern/go-libipfs/gateway/assets/README.md b/extern/go-libipfs/gateway/assets/README.md deleted file mode 100644 index 25d1a35e8..000000000 --- a/extern/go-libipfs/gateway/assets/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Required Assets for the Gateway - -> DAG and Directory HTML for HTTP gateway - -## Updating - -When making updates to the templates, please note the following: - -1. Make your changes to the (human-friendly) source documents in the `src` directory. -2. Before testing or releasing, go to `assets/` and run `go generate .`. - -## Testing - -1. Make sure you have [Go](https://golang.org/dl/) installed -2. Start the test server, which lives in its own directory: - -```bash -> cd test -> go run . -``` - -This will listen on [`localhost:3000`](http://localhost:3000/) and reload the template every time you refresh the page. Here you have two pages: - -- [`localhost:3000/dag`](http://localhost:3000/dag) for the DAG template preview; and -- [`localhost:3000/directory`](http://localhost:3000/directory) for the Directory template preview. - -If you get a "no such file or directory" error upon trying `go run .`, make sure you ran `go generate .` to generate the minified artifact that the test is looking for. diff --git a/extern/go-libipfs/gateway/assets/assets.go b/extern/go-libipfs/gateway/assets/assets.go deleted file mode 100644 index 2e442dd13..000000000 --- a/extern/go-libipfs/gateway/assets/assets.go +++ /dev/null @@ -1,203 +0,0 @@ -//go:generate ./build.sh -package assets - -import ( - "embed" - "io" - "io/fs" - "net" - "strconv" - - "html/template" - "net/url" - "path" - "strings" - - "github.com/cespare/xxhash" - - ipfspath "github.com/ipfs/go-path" -) - -//go:embed dag-index.html directory-index.html knownIcons.txt -var asset embed.FS - -// AssetHash a non-cryptographic hash of all embedded assets -var AssetHash string - -var ( - DirectoryTemplate *template.Template - DagTemplate *template.Template -) - -func init() { - initAssetsHash() - initTemplates() -} - -func initAssetsHash() { - sum := xxhash.New() - err := fs.WalkDir(asset, ".", func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if d.IsDir() { - return nil - } - - file, err := asset.Open(path) - if err != nil { - return err - } - defer file.Close() - _, err = io.Copy(sum, file) - return err - }) - if err != nil { - panic("error creating asset sum: " + err.Error()) - } - - AssetHash = strconv.FormatUint(sum.Sum64(), 32) -} - -func initTemplates() { - knownIconsBytes, err := asset.ReadFile("knownIcons.txt") - if err != nil { - panic(err) - } - knownIcons := make(map[string]struct{}) - for _, ext := range strings.Split(strings.TrimSuffix(string(knownIconsBytes), "\n"), "\n") { - knownIcons[ext] = struct{}{} - } - - // helper to guess the type/icon for it by the extension name - iconFromExt := func(name string) string { - ext := path.Ext(name) - _, ok := knownIcons[ext] - if !ok { - // default blank icon - return "ipfs-_blank" - } - return "ipfs-" + ext[1:] // slice of the first dot - } - - // custom template-escaping function to escape a full path, including '#' and '?' - urlEscape := func(rawUrl string) string { - pathURL := url.URL{Path: rawUrl} - return pathURL.String() - } - - // Directory listing template - dirIndexBytes, err := asset.ReadFile("directory-index.html") - if err != nil { - panic(err) - } - - DirectoryTemplate = template.Must(template.New("dir").Funcs(template.FuncMap{ - "iconFromExt": iconFromExt, - "urlEscape": urlEscape, - }).Parse(string(dirIndexBytes))) - - // DAG Index template - dagIndexBytes, err := asset.ReadFile("dag-index.html") - if err != nil { - panic(err) - } - - DagTemplate = template.Must(template.New("dir").Parse(string(dagIndexBytes))) -} - -type DagTemplateData struct { - Path string - CID string - CodecName string - CodecHex string -} - -type DirectoryTemplateData struct { - GatewayURL string - DNSLink bool - Listing []DirectoryItem - Size string - Path string - Breadcrumbs []Breadcrumb - BackLink string - Hash string -} - -type DirectoryItem struct { - Size string - Name string - Path string - Hash string - ShortHash string -} - -type Breadcrumb struct { - Name string - Path string -} - -func Breadcrumbs(urlPath string, dnslinkOrigin bool) []Breadcrumb { - var ret []Breadcrumb - - p, err := ipfspath.ParsePath(urlPath) - if err != nil { - // No assets.Breadcrumbs, fallback to bare Path in template - return ret - } - segs := p.Segments() - contentRoot := segs[1] - for i, seg := range segs { - if i == 0 { - ret = append(ret, Breadcrumb{Name: seg}) - } else { - ret = append(ret, Breadcrumb{ - Name: seg, - Path: "/" + strings.Join(segs[0:i+1], "/"), - }) - } - } - - // Drop the /ipns/ prefix from assets.Breadcrumb Paths when directory - // listing on a DNSLink website (loaded due to Host header in HTTP - // request). Necessary because the hostname most likely won't have a - // public gateway mounted. - if dnslinkOrigin { - prefix := "/ipns/" + contentRoot - for i, crumb := range ret { - if strings.HasPrefix(crumb.Path, prefix) { - ret[i].Path = strings.Replace(crumb.Path, prefix, "", 1) - } - } - // Make contentRoot assets.Breadcrumb link to the website root - ret[1].Path = "/" - } - - return ret -} - -func ShortHash(hash string) string { - if len(hash) <= 8 { - return hash - } - return (hash[0:4] + "\u2026" + hash[len(hash)-4:]) -} - -// helper to detect DNSLink website context -// (when hostname from gwURL is matching /ipns/ in path) -func HasDNSLinkOrigin(gwURL string, path string) bool { - if gwURL != "" { - fqdn := stripPort(strings.TrimPrefix(gwURL, "//")) - return strings.HasPrefix(path, "/ipns/"+fqdn) - } - return false -} - -func stripPort(hostname string) string { - host, _, err := net.SplitHostPort(hostname) - if err == nil { - return host - } - return hostname -} diff --git a/extern/go-libipfs/gateway/assets/build.sh b/extern/go-libipfs/gateway/assets/build.sh deleted file mode 100755 index 531bbfc02..000000000 --- a/extern/go-libipfs/gateway/assets/build.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -set -euo pipefail - -function build() { - rm -f $1 - sed '/ ./base-html.html - (echo "") > ./minified-wrapped-style.html - sed '/<\/title>/ r ./minified-wrapped-style.html' ./base-html.html > ./$1 - rm ./base-html.html && rm ./minified-wrapped-style.html -} - -build "directory-index.html" -build "dag-index.html" diff --git a/extern/go-libipfs/gateway/assets/dag-index.html b/extern/go-libipfs/gateway/assets/dag-index.html deleted file mode 100644 index 5bba8f5c0..000000000 --- a/extern/go-libipfs/gateway/assets/dag-index.html +++ /dev/null @@ -1,67 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - -{{ .Path }} - - - - -
-
-

CID: {{.CID}}
- Codec: {{.CodecName}} ({{.CodecHex}})

-
-
- - - - - - - -
-

Preview as JSON
(application/json)

-
-

Or download as: -

-

-
-
-
- - diff --git a/extern/go-libipfs/gateway/assets/directory-index.html b/extern/go-libipfs/gateway/assets/directory-index.html deleted file mode 100644 index d861cb657..000000000 --- a/extern/go-libipfs/gateway/assets/directory-index.html +++ /dev/null @@ -1,99 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - -{{ .Path }} - - - - -
-
-
- - Index of - {{ range .Breadcrumbs -}} - /{{ if .Path }}{{ .Name }}{{ else }}{{ .Name }}{{ end }} - {{- else }} - {{ .Path }} - {{ end }} - - {{ if .Hash }} -
- {{ .Hash }} -
- {{ end }} -
- {{ if .Size }} -
-  {{ .Size }} -
- {{ end }} -
-
- - {{ if .BackLink }} - - - - - - - {{ end }} - {{ range .Listing }} - - - - - - - {{ end }} -
-
 
-
- .. -
-
 
-
- {{ .Name }} - - {{ if .Hash }} - - {{ .ShortHash }} - - {{ end }} - {{ .Size }}
-
-
- - diff --git a/extern/go-libipfs/gateway/assets/knownIcons.txt b/extern/go-libipfs/gateway/assets/knownIcons.txt deleted file mode 100644 index c110530ea..000000000 --- a/extern/go-libipfs/gateway/assets/knownIcons.txt +++ /dev/null @@ -1,65 +0,0 @@ -.aac -.aiff -.ai -.avi -.bmp -.c -.cpp -.css -.dat -.dmg -.doc -.dotx -.dwg -.dxf -.eps -.exe -.flv -.gif -.h -.hpp -.html -.ics -.iso -.java -.jpg -.jpeg -.js -.key -.less -.mid -.mkv -.mov -.mp3 -.mp4 -.mpg -.odf -.ods -.odt -.otp -.ots -.ott -.pdf -.php -.png -.ppt -.psd -.py -.qt -.rar -.rb -.rtf -.sass -.scss -.sql -.tga -.tgz -.tiff -.txt -.wav -.wmv -.xls -.xlsx -.xml -.yml -.zip diff --git a/extern/go-libipfs/gateway/assets/src/dag-index.html b/extern/go-libipfs/gateway/assets/src/dag-index.html deleted file mode 100644 index 7a42ef6be..000000000 --- a/extern/go-libipfs/gateway/assets/src/dag-index.html +++ /dev/null @@ -1,66 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - - - -{{ .Path }} - - - -
-
-

CID: {{.CID}}
- Codec: {{.CodecName}} ({{.CodecHex}})

-
-
- - - - - - - -
-

Preview as JSON
(application/json)

-
-

Or download as: -

-

-
-
-
- - diff --git a/extern/go-libipfs/gateway/assets/src/directory-index.html b/extern/go-libipfs/gateway/assets/src/directory-index.html deleted file mode 100644 index 109c7afbf..000000000 --- a/extern/go-libipfs/gateway/assets/src/directory-index.html +++ /dev/null @@ -1,98 +0,0 @@ - -{{ $root := . }} - - - - - - - - - - - - - - - - - - - -{{ .Path }} - - - -
-
-
- - Index of - {{ range .Breadcrumbs -}} - /{{ if .Path }}{{ .Name }}{{ else }}{{ .Name }}{{ end }} - {{- else }} - {{ .Path }} - {{ end }} - - {{ if .Hash }} -
- {{ .Hash }} -
- {{ end }} -
- {{ if .Size }} -
-  {{ .Size }} -
- {{ end }} -
-
- - {{ if .BackLink }} - - - - - - - {{ end }} - {{ range .Listing }} - - - - - - - {{ end }} -
-
 
-
- .. -
-
 
-
- {{ .Name }} - - {{ if .Hash }} - - {{ .ShortHash }} - - {{ end }} - {{ .Size }}
-
-
- - diff --git a/extern/go-libipfs/gateway/assets/src/icons.css b/extern/go-libipfs/gateway/assets/src/icons.css deleted file mode 100644 index dcdbd3cd9..000000000 --- a/extern/go-libipfs/gateway/assets/src/icons.css +++ /dev/null @@ -1,403 +0,0 @@ -/* Source - fileicons.org */ - -.ipfs-_blank { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAWBJREFUeNqEUj1LxEAQnd1MVA4lyIEWx6UIKEGUExGsbC3tLfwJ/hT/g7VlCnubqxXBwg/Q4hQP/LhKL5nZuBsvuGfW5MGyuzM7jzdvVuR5DgYnZ+f99ai7Vt5t9K9unu4HLweI3qWYxI6PDosdy0fhcntxO44CcOBzPA7mfEyuHwf7ntQk4jcnywOxIlfxOCNYaLVgb6cXbkTdhJXq2SIlNMC0xIqhHczDbi8OVzpLSUa0WebRfmigLHqj1EcPZnwf7gbDIrYVRyEinurj6jTBHyI7pqVrFQqEbt6TEmZ9v1NRAJNC1xTYxIQh/MmRUlmFQE3qWOW1nqB2TWk1/3tgJV0waVvkFIEeZbHq4ElyKzAmEXOx6gnEVJuWBzmkRJBRPYGZBDsVaOlpSgVJE2yVaAe/0kx/3azBRO0VsbMFZE3CDSZKweZfYIVg+DZ6v7h9GDVOwZPw/PoxKu/fAgwALbDAXf7DdQkAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-_page { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNpsUztv01AYPfdhOy/XTZ80VV1VoCqlA2zQqUgwMEErWBALv4GJDfEDmOEHsFTqVCTExAiiSI2QEKJKESVFFBWo04TESRzfy2c7LY/kLtf2d8+555zvM9NaI1ora5svby9OnbUEBxgDlIKiWjXQeLy19/X17sEtcPY2rtHS96/Hu0RvXXLz+cUzM87zShsI29DpHCYt4E6Box4IZzTnbDx7V74GjhOSfwgE0H2638K9h08A3iHGVbjTw7g6YmAyw/BgecHNGGJjvfQhIfmfIFDAXJpjuugi7djIFVI4P0plctgJQ0xnFe5eOO02OwEp2VkhSCnC8WOCdqgwnzFx4/IyppwRVN+XYXsecqZA1pB48ekAnw9/4GZx3L04N/GoTwEjX4cNH5vlPfjtAIYp8cWrQutxrC5Mod3VsXVTMFSqtaE+gl9dhaUxE2tXZiF7nYiiatJ3v5s8R/1yOCNLOuwjkELiTbmC9dJHpIaGASsDkoFQGJQwHWMcHWJYOmUj1OjvQotuytt5nHMLEGkCyx6QU384jwkUAd2sxJbS/QShZtg/8rHzzQOzSaFhxQrA6YgQMQHojCUlgnCAAvKFBoXXaHfArSCZDE0gyWJgFIKmvUFKO4MUNIk2a4+hODtDUVuJ/J732AKS6ZtImdTyAQQB3bZN8l9t75IFh0JMUdVKsohsUPqRgnka0tYgggYpCHkKGTsHI5NOMojB4iTICCepvX53AIEfQta1iUCmoTiBmdEri2RgddKFhuJoqb/af/yw/d3zTNM6UkaOfis62aUgddAbnz+rXuPY+Vnzjt9/CzAAbmLjCrfBiRgAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-aac { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNp0Uk1PE0EYftruVlvAUkhVEPoBcsEoLRJBY01MPHjCs3cvogcT/4qJJN5NvHhoohcOnPw4YEGIkCh+oLGBKm3Z7nZ3dme2vjOhTcjiJJvZzPvOM8/HG2q325Dr3kLp7Y1ibpIxjs4KhQBZfvV6s7K5Vb0bjeof5ZlcGysP1a51mifODybvzE8mzCbrAoTDIThMoGXZiZ4YSiurf+Z1XeuCqJ7Oj+sK3jQcNAmg8xkGQ71mYejcAB49vpmeuzJccl0+dUj6KIAvfHCPg3N+uAv4vg9BOxcCmfEzuP/genpmeqhEMgude10Jwm+DuUIyUdTlqu2byoMfX/dRermBeExHsTiWNi3+lMpzRwDki8zxCIATmzbevfmClukiP5NFhJgwkjeRTeLShdOoVJqnAgwkgCAZ6+UdLC9twjQZ8pdzioFkZBHY3q6B3l4dJEEEPOCeD4cYVH7Xsf15F+FImC775INAJBJSkVoWo0QY9YqgiR4ZZzRaGBkdwK3bFxGLRZUfB3Rm2x4x9CGtsUxH9QYkKICDFuLxKAozGZwdTqBRs2FbLlXbiPdECMCHadj/AaDXZNFqedCIvnRcS4UpRo7+hC5zUmw8Ope9wUFinvpmZ7NKt2RTmB4hKZo6n8qP4Oq1HBkKlVYAQBrUlziB0XQSif4YmQhksgNIJk9iaLhPaV9b/Um+uJSCdzyDbGZQRSkvjo+n4JNxubGUSsCj+ZCpODYjkGMAND2k7exUsfhkCd+29yguB88Wl7FW/o6tT7/gcXqAgGv7hhx1LWBireHVn79YP6ChQ3njb/eFlfWqGqT3H3ZlGIhGI2i2UO/U/wkwAAmoalcxlNA1AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ai { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk5JREFUeNpsU01vElEUPTPzZqBAQaSFQiJYUmlKYhoTF41L3Tbu/Q/+AvsX3Bp/gPsuWLrqyqQ7TUxMtAvF1tYGoXwNw7wv7zwYgtKX3Lw379575p5z77O01ohW+/DVh8zj7aYKhflGdG9ZsGwLNydffgVfr19YHvsEa+Zu/nxndob5StQK+dyzvZzyw/gKlmMj7IygFM+xvNcanp4/t5dAomXHBy2UUBOO2MAl/B9/cPb6PULuoHx0WM0e3GvpUOxD3wZAJWutZqYUYmqpSg5OMgH3YQObL59W0/ullpryR3HegkKEqiWBSGV4R3vQ7sIhScTZFTpHx3A215B5sluVY/WWMg7+ATB/lcLsKpTonHzD+OMFEuTz8ikkt9Kwt9YJZB38cpBdoQAZJdLvCGByfoPB6Xdk90pYy6Xg3c/DaWwArg09DaG5lCsUFN0pckZAojdC8m4auBqaALuSgez7VB1RtDSUWOQvUaBLFUzJBMJ2DwmPgd1Jwm0WoSgJfjDvrTKxtwAIyEkAOQ5hU//Zdg5uowDlUNMnwZLW0sSuUuACYhwQRwFvJxupCjEYUUccOkoaKmdOlZnY1TkgAcXAhxhOwLsDsHoN3u4O5JTDfVCH6I9nfjId3gIgSUATFJk/hVevGtOMwS0XwQ3AzB/FrlKg8Q27I2javVoZrFgwD4qVipAEyMlnaFArzaj/D0DiMXlJAFQyK2r8fnMMRZp4lQ1MaSL5tU/1kqAkMCh2tYI+7+kh70cjPbr4bEZ51jZr8TJnB9PJXpz3V4ABAPOQVJn2Q60GAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-aiff { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAohJREFUeNpkU9tqE1EUXZmZpE3aTBLbJFPTtFURtSCthr7UCyKKFJ/9An3og6Ag/oXfoUj7og9asCBYKT6UIPHaWtpq7NU2aZK5z5wZ9xxMpMwZDuewz9prr32ZiO/7CNaDx3OLt6fOjBqGg/aKRCIInp8+KzfKH7fudnVF58nE16el+/yU2mBFSWZKpWJKVc0OgUBo02K4NDmU6o75Mx+Wdu9IUXFeiOA/pn1xHeYaugVDdzpbp91qGlAKGTx8dC19/Wpxhjnsxj/RRwk85hGJC9d1O6fneWAuoztDYSSLe9OT6SuXB2ccx73Z9uukwDwfls1g0xZIY/Ad/Gnyt/XVfbyYrSDRE8PExHB6/8B6QuaxIwRBFMt0iIAiMx+LCys8jfGJEUik2WpZOD2SQf9oDtVqQwopCAiY66FS/om3b75CVS2MlU7AJ2WiJBCZjZ2dJuRkDJZFwFAR7UCBja3fNfxY2YEoCtRCj9em3Tpds6FpJseGCBxS0GgYGBzqw62p84gnYnAI2CSbSbPhEpFAaE2zODaUAlWWwDoS5DheGqbWpVE/0CmqCY9qkEyINBceb2uADRNQ8bSWAVVzIFKomCQim+0luS4yKYlsHlRyZo7EsSEC23K5vAsXh/H92zZkuRvxeBS5nEx2yp2KqhxPoV5TYS/8CtdApylM9sZQKKSQzyeRTseRV2QoAzIYY8jme5DN9fI0dQoUIjANGydP9VM7PZw9p/AiBpNYrdbw/t0yTJqRtdU9UrfJCUMpSJIgbWzsYe51BcViHzLHeqCRqhZ1YX1tFwNfZBxS9O3NWkAcHqR606k/n/3coKAoV/Y7vQ/OYCZevlrmv3c0GsFh06u3/f4KMABvSWfDHmbK2gAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-avi { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm1JREFUeNpsU8tu00AUPXZcN0nzTpq2KQ3pAwkIAnWHqCoeexBb+AQ+ABZ8A2s+AIkdm266QUJIFWKBkHg1KpRHi5omJGkbJ3bGHj+4M1EQrTvSyGPPueeec++1EgQBxHp+/9mbyuriRZdxjJaiKBD3W+u1+p9a856max+gDO8ebT+WT20Ezi9NZi/crqadvn2MQBAGfpCOpqNru2937vxPIpY6Onjccx3Twck9MBiSU0ncfHirXFmZX3Md9wqCUwiEVN/zaQfHt0vfbBe5uQyuPVgpl5Zn11ybL4/i/lkICOw5niQRGQShoiqI6Bo43W2ub8n3hRtLZT7gTynk6gkCX9gAOxpAnxhHZDwC1/aI1EViJolu/QhKRMHZ1UX0Gr1USIEn5FPWHy+/wTokkrQOq2vBaHZBN4hmY9Jwfr4An/teiEB45ZZDwDiMhoExT0N+sYDCuUkkplLIlXP4/XEXdo+RUhdhBSSfUwtVTUG8MIHK9QVqI7D/uY6vr2pwmCPrkz+Tk9gwARWQ9WxppbXZhNnpw+ya4A5HZi6L4lIR8WyCcL6sTZiAWjWgAmpxkn5+kqTamK6WkCwmERmLDLvjB0ML9ikWXPLFuozYOap3L8HYN6DHdbS/d5CeTVBndBz87FCBLYkNTyIjBQemnIEsSY5lYrK1+UoWcToLMjEHAyIQ2BCBSx/NVh+ZUhrqmEqBebS3WyhdLg0zt/ugAaIklsSGLHCLa6zDMGhZ2HjyGsnpFPqNHnY2fmHv3R5SMymYbROszSQ2ROAY9qHiofvlxSc5xsKKqqnY3diRE9h4X5d/pzg7lnM4ivsrwADe9Wg/CQJgFAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-bmp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmZJREFUeNp0U+1rUlEY/13v9YV0vq2wttI5CdpL9aEGBZUDv0df668I6n+or0UQ/RuuD0EgVDAZrsKF4AR1a6COKW5qXvXec27PuVeda3bgcF6e8/ye5/d7niMZhgExnK9fbTrm5pbBGMZDkgCyq+VyhTUaT6Eo2ZHJePPWXJXRhez3B1yxmM/QdctXUSCgtV4Py4CvY3cky4e1x5DlLCaGbbzjXDcousG5OQe5HPRSCQPK4PpsEM/XH4WvhS4noeu3JwHGGRiULhsMoKZS4I0GtEIB9mgULJGA0+9DPBpBT7sffvf1W/Lg6OgJufw8C0CRGEXWazUwiiyFQjA8bsjVKjaJzovMD/Q5gxyJhG2cvyeXe2cAuADQNGBmBvLaGuTFRaDfh31lBTWi9pumjbK0B4JQul3vOQpM8JdskOLrdCvDcDjAsjtg5TIkoiKLaokMNR2cnZbqNAMycqG7XbHKR2fMzwO/dsxSwu0BiBJsNsv2LwAJAJCI5ux2gXYbqNetcz5PoORI1cDS0n8AxGW7A+zvEYBKZ2ZlcsEtJLbedMjePBaCTQMghx45ulyWkzxMVUQ2RMQhLfFO16YAqCrixPnm6iqKrRb2W23EfF4cUNSrHg90cr7hDyB33MTnSmUKALVs4uIlROjxg+AsPhGVl3fuIl2tIOB0Ya91gkOi9mxhAal0ekork1ic/kGLBORMxy2K1qS9V1ZQbNThIj2EGh+2tsyOnSai8r1UxMNIBB+LRTTULr4Uds0K1tU/uOLxIrmbNz8XXSrnASSpubG9fbKRyVh1n/zSw29t9oC1b47MfwUYAAUsLiWr4QUJAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-c { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcxJREFUeNqEUk1rE0EYfmZnkgoJCaGNCehuJTalhJZSUZB66a0HwXsP/Qn+FM+9+hty0LNYCr2I7UVLIW0Fc0hpQpSS7O7MrO9MspuvVV8YMnk/nn2e5x0WRRFMvP/w6WSz5jbi/9NxfP693Wp3DrJCnMW5d28P7a+IE15lufR8o1ZEStwPhkWHsWbrZ+eNEPxsuubEF6m0TBv2Q4liPofXuzveulttSqW2UwH+GjqC0horpSL2njU89+FyMwjlTlxOJMTa9ZQHzDQIjgwdom9zLzfXPc75kbnOAswBJTlC2XrqQRMLxhi442DgB4UFBhgPpm3B5pgBHNUUxQKAHs8pHf3TEuFMetM9IKr/i2mWMwC0SnuSFTG2YKyppwKYVdGO7TFhzBqGIenVeLCUtfURgErucx5ECKREKBU4d3B718PHz6cICGT/1Qs8qpQtGOdyhtGEARWDQFqQJSeDL98u4VbLaKw9IRAJPwjtoJGlVAoDQ800+fRFTTYXcjlcXN2g++s36p5Lzzlve1iEROa8BGH1EbrSAeqrjxEqicHQt8/YSDHMpaNs7wJAp9vvfb287idboAVkRAa5fBYXP9rxO4Mgf0xvPPdHgAEA8OoGd40i1j0AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-cpp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfJJREFUeNqEUs9PE0EU/mZ2WgqpXX+QIDFdalVslh8NlAOQaOKFAwfvHvwT/FM8e/U/MOnBmwcj8WD0ACEGghIkbU0baaEthe3OTJ0ZWV26q37JZt68ee/b9733yGAwgMbL12/fz+azbnAPY2Nrt7Zfqz9JMrYZ+J4/e2pOFjiciRvXlgp5GzHonXk2o6S8V6k/TjBrM/xGA4MLyeOSPZ8jkx7D+uqCU3Amy1yIYizB36AlCSkwfjWDR4uu40yMl/s+XwjeWThQQ4Z6QNSnSkYykcDXasP4lmfvOZTSF9q8TDBEFPbN5bOqCglCCCxK0TvvZyIV4CIxbgpC+4gm/PUmFCIE8iJPyME/e8Lon9j4HvyHYLjKSwRCSEUgf9+15mFbx8QS6CZJMzJ9SlBCwX3fJDLG4PX7ykcwkmQmJtpEhWa7g1dvNlSwjwelebz7tAXLolh0p/Fxe9fErK2WDFGEgKjxfNjegX0lDTc/heNuF99/HGEslcKXwyoazWNDdlCr6+DoJgrBzdI0T9rYO6yg2zszMlaKM3Dv5OBzbuyZuzm1B16U4Nzz2f3cFOx0Gq12F9cztpExncsqYoaHpSIKtx0zJdVIFpHQ6py29muNk1uTN829o/6SHEnh80HFaE6NjmLnWxUJy1LyTltB3k8BBgBeEeQTiWRskAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-css { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAk1JREFUeNpsUktvUlEQ/u5DoCLl/RAKKKUvWmIxjYntQtcu3LvwJ/hTXLt16coFC2PsojEaMKZtCqFaTdGmjbS0CG3By+vei3OOBSGXSU7uzNyZ78z3zRF6vR6YvXzzPrMUCyf68bB9zO+VfpROn5hkOdfPPX/2lH/lfiLidztX5mN2jLGG0rKLENIE8liWpdzwP7HvqJqujmvudFU4bFY8Wk1FZsOBtKppd8YCDNu77CZevd3gflfTUFcUhP0ePLibiIR9rjSBpgwAfe4dVcV6dhtep4PH5msylGYLrzeybErcT85FYiH/CyPAf74gObC2vMhzsiRhPhpC6eQUM+EA1pJzILEnjRSuJsju7MJqsUCSRei6Dp3yXqcdGlHZ/rLPazQWGCn8+6YW4pAkEW0SjzUzanWlCa/LgcR0lNfovTEi6lcIkzesnM/R8RlN0INGp3h4DHoDsE5YRvQyiKiRSMzikRAOS2WoqoZWu41K7RwzlOOAVDMMMHhIGvFlRxJFrKYW0ep0IYgC3SDh4b1lTJjNfENsrazOAMAw680mPuW+8lFno1P4XDigRhOiwQAyJK7TbsNS/PaA7giAIAhYz2yRgBIfsVA8wIetPG6FAqhdNrC5u0f+TUyHgyMTDDToEt/ftQsEvW4EPG5OZcrvw0mlimarTXkPfpXPcNlQoGtjACgpryQXsPNtH/nvRXqBJpoKHMzGNkNB0Odls7LNyAYKpUq1dt1iuvB7fRDp9kr9D1xOFwkpoksXusmXaZWFn0coV89r/b6/AgwAkUENaQaRxswAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dat { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfVJREFUeNqMU01PE1EUPe/Na0uptmlASg3MoiZgCA3hQ8PHAjbqwsS9C3+CP8W1W/+BSReyYUPwI4QAVkAgUEgIbVIg1FZb2pl5b3zv2cHBjsaTTOa+e989OffcGeK6LhTevFv+OJoZHPHOfrz/sl86KpWfhxnLe7lXL1/oN/MSZqonOXU/k0AA6lfNhEFIrlAsP2PMyPtr1AscLpyg5pbtIHErhqez4+awmc45nI8FEvwNaiQuBHqTcSxMjJhmX0/Osp1xr878FxWEzwMinxAzEA4xFIpnOjedHTKpYbxW4U2CP4j8uWxmUKsghMCgFI2mFe9QgHZj0Ba4yhFF+KvGJToIRLuPC/efnjD6+26wB1Lq/xgbSCBXKeWJG/OTdky8cWTdT3C9RmWSGk2XCLlWo4xTNbfN5qh7PpXM72GjZeHt0gpq9QbmH4whGb+NpU/reDQ7hcWVVXxvXOHxzCQopQEKXKEbL6o1ZIcy+LC5g62DY2zsHeC0fA4zndIrHOjvg2XbAQRSfsuy9XxC2qzi/H5B6/68W0AsGkW0KyJPBLbDO0fg3JX/CUM81i0bD6WKe6j9qOPJ3EMcF0tSNsFA6g6alqW+VtZBUL78Vtk+Oqne7U9rs5qOQCjSheJFBeFIFOfVujSUYu3rIc4uqxWv76cAAwCwbvRb3SgYxQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dmg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAn9JREFUeNpsU01rE1EUPe9lkk47yWTStCmtNhFSWxos2EXVhSsRcasuxYV05V8Qf4DgD/AvCK5EV1oFI7iUBqmCNdDvppq2mWSSzEzy3vPOpFFq+uDNfR/3nnvueXeYUgrBWH1/9/NE7k5BKRnuRcfF2qdnmJq9DeF9tQ+2isuMsxXGWHh/a1mEVsPJSI5fSU3OPEj291IIlN49RXz0KqzEQjIeZS/L5Y/3wPGhDxIM/i/A7fZWgVG0t5EaG0ZUa0JGM8gvPrZmLt58QYwv91mfAqCIE0sAqgumBFITGQzpUYhuF0KfRa7waDyXXXolpVrsh/0tgSLDr5I+wUZo1UHCSkAficPzY6juFSmbRPrC/azjq+fkcO00gAqoU7B0ETKkfWbuCTjTYeq5oESAauexcTScX+ZACWFm0YQSLZKhHdr67+/wW0e0dgjYo3sCEXXybYtBDVSHLp2es3IpsILS24c42lkBg6DzRjgRzCDZ/xr0GNRJwwYiWgzt+hYMawleu0V3wbkT+kUirOc7IGJAz68R/Qak1BAlx3hqASPGBJRXpXOv58dkz3eAgQoOm4hyj57NgZm0MHvpBmK6QdUdg/DAg9cRkhicBSDaKJdeo1bdxmR2DtWDDUxl51HZ+QHTysD3XdQO95Gfv06aeGcAdBrY3Chi8lwO3768QWX7J5q1XWyVSxgajiOXLyBG2hzurRKV9lmt7ISNkkjo6HhNyjoK+2gXRsKE57ZIE2ot10Z1fz0Ue4ABVw3NMjnW14rInh8jTYywoTg3EOFpOM4mXNfH9PQUfGlrAwBOs3I8ljbtuMWhRWzIIPrkn+GcYcgIWEowbZ+0qB334/4IMADESjqbnHbH0gAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-doc { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAppJREFUeNpsU79PFEEU/mZ39vZu77g7DokcP04BBSUmiEKCSCxs7Ei00JAYO2NlTKyMrX+CJhaGwopSQ0dMtFEsbDRBgiZEQIF4IHcg+2t2Z8eZ5QDlnM1mZ9+8973vfe8NEUJArfSNhzPG0VIfeIiDRSDkw1cWVt3N8rhG6SdSO2Gvn8dfuueqZwuNZqk3Jxg7iNcIfBbgXD6ZC8u5qffzX8eoYeyDxC77uygKhcouovgVUQj1H4YB2ovNuD9+tTTU0zMVBmG/+C8AIYh8F361DL/yE5HnADKYlVdg6MDAmW7cuz5WGuw+PsWDYGAvbL8ECFUt4K7/AHd/I9c7BLaxinD2Ld5Zo7g78RLuRhlBS2cpWbGfStfhfwCEpK0nUjCbWuGsLciSOELPhkq/YgdY3l6HsLfRcLYf+pHNbH0JigEPkLAyMsiEJ7NrqQzM1i7wyhoMZqOhvQs6Z0ovXgdAJACRoulEg5HOwrOroKk0zOY2BDtVpTF0CU6kLkQJXa+BNEoG0lMSsBBKQXWNQktmoGcaYeSaQCIVWOvUYQAiWZFQtk5mSMoSzEILtBrTfEcviC5bwVwQmoh96wA0ic5dB57ngeoaTIPCdb34zDITYNLOOIeVSsW+dQC+7+NSWx6jJ4tY/rWNV7PfcGv0tBoPTM7M4eKJVgx2FTE9u4QPS6x+kHzfw/mOAjarW2hJG3hy8zIceweuY+PRtREMdzbjzcd5WBqPB6xeRGUMGRzHjWvMmxQ7tiOF1JBN6FiTd6Sy9RuFbHpX7MMMqOD088Ii+op5OUAO7jyeRGfBwrF8Cg8mXuDL4neMXzgFwhwZz+hf7a9d5yu3Z6DTPjVQIY9k7erO7Y63Lvc8ErEeyq6JaM6efjai4v4IMABI0DEPqPKkigAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dotx { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAndJREFUeNpsU01rE1EUPTPzJk0y+WhMStW2qdVWxUVEQUF0I+4ELQiC7lz4N9z0T+hG9wrdZKUgLqulhrbSag1CKpT0g7RpYjqZmffle5NEKdMHlzfvvXvPPffcO4aUEno9f3Vt4dTp+BXOe+fB0u/NbVpv7h89NU1j1TCM8H7+xY9wJwPHZMbOjRadLAvE/2gToJTiTPx89k+OlVd/LT+0TPIPpO/SzyQk40xCMxBSZ9Z3CoAx5DOjeHT7SbE0XSpzwa8OWB9jINELolQg8AR0EgUKn1PIlIWpkUt4cPNxkTOU12trs8p95RiAXpqaztqou8q6SKQJJmZSqGwsodFsIJk1kcyLYv7IeafcLx4HUNkFF4jFTExMZ0B9DrfD4HUEusYhWs4GPEJg5wly/tBYRIOeDhpEwlS34xcyajdQr3UwOT2MlJOEBRuGNHWp9AQRVXDfQiFV/U5GBSiQ5p6ngBEa5z3fiIhC6g6IMDBwOdoHPkYnHPVyhN0tF7E4QSpr94CEOKELffq+y9Bq+DCJ7rWBoQQBVbPR2O6G4OlsLASJMtCZfQqm0NP5IVWnamdAkUxbyuIYtD7wWegb0YAzAVMkkI6NwPM9xEwHloyDGAmk7AKS9rAS0FKOdugbYeAHPu7OPEM+MY7q3hIKqTFQHmC3XcONc/fxdfMDrk/ew/edzyhvvTmBAddocVRqH3Frahau56qpZDho7+PnTgXffi/gbHYmLEvPSIQBp5JU62sYz13G609zKBXvoOMdYn2zgm7Xg2MVML/4Eu3uPgxhk2gXmNl8v/i2pcXTP8tKdTEcbWLZqDQXwu/l6pfwbEnSGsT9FWAA4mdHv2/9YJ4AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dwg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAoFJREFUeNpsU0tPE2EUPfOg006hD4rQh8WgbCSwkKgbF2owujaCiQsXxpX+D6MmbtXEsHCLmIAbE6NLo8YlGIxREIshIqVl+mQ6j8/zFVCb4UtuZua795577rl3FCEE5Bl79vPd5LHYiOP7cH1AUWi85ytmvlas1bJ9E5ryBntH3BpuP/X9i7ovkluuiE8N9SDepaLpCcRCCqa/VDCaMuIjSWP25Upl6n+QDoCz6Yh7KKzh3sI2LuUimPtRRyaqodj0MDloYiITSTi+mH29Wu0AUf9CsZPJoW5czJl48LmCc5kIKo5Al67B9gUGYxrun+5NnMlFZ+GKiQADj2a7AquseLIvjMv5KMaSBu4sWVir+3i8VIVKYSby0UTdFU8Znu8AYBHQgVOJEN5uOXi4UsdawwU0FSf6TaSoyw6DRvukPkgGWpDKy4F8a3jImCrqFDFn6rhKPR4VGnhvOTAY3WLcjifcQAsqRfhUc/Gq1MKNbBh9nIAMDjEppocxs9HCMktfGTCwP/oOBkUKNk/qF3pDYC6Ktk8RfWzyaaoKrqdDaBDwya8W1m0/CPCR3kFy7CcnmWQRUJqcRJFUKtTnPCeR71LwoeYF92CYyVnCFZpCTrRtCv5to2St8SOrKxiPqEEA4fkYT+mI0rdoeUiH1XZVuQPpsIKqw2QmfifTsnOABiWySlH9uU0Hh2MqjsZV5LtpPSoGeN9rKnhBX7ehoOSLIIPfnGONXGMMWN7xUfVldYDbjM3mrh5HCDgS17DhHgDQcIU+XbBxnDTn1x1UuQcJ9iv7l5Q5e1zLGri92EDJFnoAgHtcfr6wbbVXUqq193+0z97n3UJt1+d51n7aHwEGAAHXJoAuZNlzAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-dxf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo5JREFUeNpsU0trE1EYPfNMmtdoH2kDNmJbaVFcaBVFpAsREQpFwY0bu3HjQnTj1mVd+ANcuC3qQixmry6E0kWFVIQ+bKy2tbFJm3emyXTujGca+4DkwsedfLnn3POd77uS67rw1vC79ek7fZEzpu3AYUqS9tKQGZPLpa3VXP0uFCmJ/8t9OLC3q/uJbcs5bkIybvdHoMsSbLKENRmvU2WcNnTjRFD7ML1WGSPJHI6sA4KRWMAWVDPxLYex3iCmfpuIh1QsFSyMxQO4GvXHHwOJ6XWSyIck8v6HQsnjAxFc7vTj2VwBg4aG78VdBHQFCk+dbVcxMdwev9gTSEC455sIBOu2KLsoJFzqasP9vjCeDBlYqzn4VXXwarGKZN7Crd5QfLDT/7KpBM84c9fFUFjFp2wdk6smflRsKKqMa7EgfJJ3Ac2OKlit2pEmBTQfngdpnupoU7BUtRGiiTe7fXiRqmK+KuDn6TpvYogmBRJcrOwIJLIWxmM+dOsyLKryQAaJpjJ1/AxrGO3SqdZt7kKZJrzJWBg5piHENuY8vV6e0UOye1TyftvC5l+gZB8SHJTwpSx4q4JeTUKaxhXoR57h7Rn+3iFolJ3xvPhab6HgJG/pJ7jsNP4sUX+jZiCgEsWd/DjH5IrSYpBUAr0yHpzSoXKOP25a6OBhndh0zcX1qIYM2RIbu6i0KiHD5B/GTMHG03kTGpEL7H80wHFOWwhqDZ+SpkBOtCDYJDhZE4gRcKNbYynAqbCMbXpwpVPFbEng0aKJGbYzK1p4wIegLlcEPmdt+DjXbzcsxFlCynRwwVAwW6hjqeg0Zt521SYCWCJvbe0Un29UDx7Hgrs3IEitHXkw3jOv2fl92D8BBgAJeyqBh90ENQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-eps { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNp0U01vElEUPfMFCEVArdoSqEA0KV246UJdUJM2Lo2JK/9FjXu3utJqTNz4D9worrsQExbFpAFT0TYp0CZ8pIAiyMfMvBnvm2Foa9uX3Lw7c98979x77hNM0wRf7ufPsq7Z2SQYw2QJAkDxQalUZa3WI8hy3gmZr15bu+z8kILBkCeRCJi6bufKMji0NhwiCQR6iitdatTvQ5LyOLLEiWcYukm3m4Zhmbq1BX13FyoxuH7xAlbvpqKRK1fT0PWbRwEmDEyiy1QVg/V1GO02tO1tKLEY2PIy3KEAlmJRDLXb0TeZL+n9g4MHlLJ5HIBuYnSzXq+DlcsQLk/D9Hoh1WrIUjlPcpsYGQzS3LWoaBhvKeXWMQCDA1D9pt8PaXERUjwOjEZQFhZQp9L2yERiqYRCkPt/z58ogTGqHQLE1BLgUmC6XGD5AlipBIFKkbhanKHGYLBDqQ4ZED0OAbfLlo8OIxwGvhVgyTHlA3xkomjH/gegBgDURMv6faDbBZpN+/tHkUApkdTA/PwZAPxntwdUyjYA/+ZMqJHjLgM9iv/6zRt2GgMaIE21aVIjnSm0DGPfmhzyde0UAE2Dj+p7urKCPvkZku9eJILOSMUnkvVhIo7GYIB3xSKYdhoA1erXGVKXpvFxZwdBonnD68PQ7YEwM4O4xwMPxc8RYE87g4FIcz+kvfmnA0YzIJIy77/m0OCqsTkkCTysKPjJG3viLei63Gm3kCO6UWqcMejjxecMPmxsoFKtYop6UNirYL9Wtc5OHqzznIXHq1na7OfMJROcK8a6O7MjW7nfzZdrd7jzT4ABACh3NGsh3GcdAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-exe { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAo1JREFUeNp0k8tPE1EUxr+ZzvRJO62lUAQaKIQ0FVJFjBBdoIkrDDHuXJi4NnHtX+HCjW408Q/QmHTRaCRRohIJifgiiBICTQu29mHfnc7MHc+MlECKdxZz595zf+c737nD6boOYzxJLC6Nhwej7e/24HkO779s7G6mMjcEwfKZ21+/d+em+RbagaFev28qEpZwzKg3ZckqCPH1nfS8hScIdyhBe6JqTG3PfyTTeLrwFhvbKdy9/xi5QglXL0yGJsKDccZY7LDIAwWHpSferWBh+RN8ni4UylVER8MY6PHj0uSpUK0hxzfTmWsUtnoEwO3rer64jEyxim6/Hy67DXaHExvJX3jw7CX8XjfORUdDlOohhU4fAVjILCPbm9V1yIqK2FgYt+ZmsZcv4lH8Nb5upXD7+hVMjIRQa8qeDg8UTYPU5cTcxSk4nS709XTD53ZhpD+IYMAPj+TBz93fZiz5oHV4AP1fGdlyHZIkIZkrI7GyhnK9CZXy+Aig6p1+HQAY003AcF8AVtGGfLWG9XTO4MLZ5cL0WAixoT4zVmPHADSiMo3hzHA/xgeDWFjbNg8H3A7kKnX0koEcPdTu/ylgRGZgOjNv38zoSXC8BZJDRKOlwGEV0VJVGM0y4joAPO1spXbx6sNHeD1uRIYGUCxVSRlDt1fC8rfvcDnsmJ+dOaLgoAs6AVLZPJJ7WdhEkUyT8GJpBflSBcVKDTvpDBw2GzQqQT1OgaZqUOhtFQUTUKnVTVWNpgy51YLVKph7sqKYkA4A1ScEfT66vm5kC3+ofh6Xz59FQ5bpkvE4QW3M5Apoyorhl9ABIKnFgNdTOh2NkJG6WSf9eRBJtmFwLDJmriUzeaOkYvvcXwEGAIVNH6cDA1DkAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-flv { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsUl1PE0EUPbssLYUCXdpaC9gWoSTgAyFigiRGY+KjvuuTr/4A44MP/gx/gMYfwIsan0RjIjGiJIZgSIGFIoXSD0t3Z3dnd70zpITazuZmJzP3nnvumaMEQQCx3jx69SV3a3KWMxetpSgKxP3m242Do43SQy2k/YRydvds67n8a63k+FRSn7l/bdg5tdsAuM3he/5weDC8vLdqPLgIIpba2niux52mg//DqlsYSg3iztO7mczN3DJ3+ByCLgCBH4hOFEF7cDpzPCRyOpaeLGXSc2PL3HbnW3XaRQCPEgWI2MsRVAVqrwbX9bHxbhOKpiJ/bzpDOr2k68V2BtRNzMtqDEqPejY/4zSGjb54BM0mQ8k4xsDoIMauXxnqYOD7PmwScP31d0SS/eAuh1lrolFpIBQNQw2pqJdqsAlIceB1AJCIkkE/FZskXDQVRXw6IYHiE0nBEcaPXSSvJnGwWkQXAE4acAhbxPMJpOdHweoMhc9b2F8zwKizbdlyPLVH7QLg+JKBYzoorxzjz3oRzUoToaEw9KyO8XQW5AE5jrFT6AbAYVVNxCZ0Ka3So+DSTAoDiej5ywTySbls1OEDobhFlMcXxrHw+AbINEjNXgb7y6BndLhk8cRkHHbD7g4gEhiJFxsdhrDqaamBaDKKerGGSKwPI9kR9EZCaNA5ubE7A5s8IFhsrxQkgJhZoa/06xC5xRz2v+3BOjFlbqcGlquxsondT9vY+2pAJdeZR6fI355CgQCN2A4O1w7gkQ7cdLUOAKdhV6uFSv3kd/n8mT68eC8dKWLnY4FsfeZQh7nVVt0/AQYAsf5g+SvepeQAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-gif { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmVJREFUeNp0U0tPE1EU/trplAqlL0laiw40xASByEJIZFGVnSvj1j+gWxNXJq7VrbrwF7h10cSNhMRHojEuACVBKmH6SJQyJeXRxzzv9dyZPiCtN5lMe8853znf953xcc4hztDzZ1+C6fQMHAfd4/MBFG+p6h/n4OAeAoGNToi/eOm+A50LKRaLh6amoty2vVpZdotNXccMEK3LwZxa2bsDSdrAqePv/mLM5tSdMwYBYqyvw9zdhUn/L59P4OGtG8qlZCoH254/DdCdQBCxqZu+ugqnWoW9swN5ehp2NotgIo6bGQWGtaS8+vQ5V9a0u5S+1gfABEilAqdUgm98HDwUQkDT8JXoPPq+BoM5kCYmFT9jryn1+hkAt7heBx8dhbSwACmTAUwTgdlZ/CVKJaLnI1GD8TikZiPSR8Gxib8chH95mZTxgwWHwH7+gFMswqcokIRbjMO2HDCnZ1VvArpjEmnKZc8+cZJJYGsLsMiZ8AgwEqaY6Mb6RQR33JFhGECzCRyfAFXNu9v+RVNRZWIMuDJNuYMAaDycUFGhCOgtuAtFVDA83G5A8TrFDw+F5QMAxAKJJxz2xnW3RPJGbm+rCyjotZetH4DGzaSSeDA3h4Zl4R0JOEZWTpIzF4n/m995bNdqZwB6m0gFft3Ak6vz+KYWwFsGlqIxXItEcDt1ARMEtKdVgZb+fwA0G2C2hXM0ZTZNRcSf0b1pmXi7uYnjI+Lfanm5fRQsK8BIxKcrK7i/uIgP+Tw+FlREqHN5fx/vyU4uHBE6UO4gDWqk/JFaLuMxcXeFk6TuJ90V0HOk1in7J8AAjmgkPfjU+isAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-h { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAbRJREFUeNqMUk1Lw0AQnf0woK0ttVqp0hwqVCl+UBERT94F7x78Cf4Uz179DT14F8WbYHtRkBYRLNqDtdaPZLObuLs1NGlXcWDJZGbey+x7QUEQgIqT07PL5WKhHL5H46J+22q22vsWpbWwdnR4oJ80LNiz2czGUjENhvj4ctIE4Wrj8XmPUlKL9nCYcOFzE9j1OKSTCdjdrtiLdr7KhVgzEvwW6krC92E6k4Kd9bJt57JV5vFK2KfRQRV+RAMkzxglYI1RaDy2dW1rpWRjQo5VGicYIorWVooFvQVCCAjG8Omw1MgG8AM0uSBUDSnCfk/IGCHwf3DCD/7UhOLBrFkDuep/hDUSSCv1iYo4rIfqGwmUSNJjfYbBcQKhZw0aBMA4B48LwBhBt/cON80HmM9NQ6fXg/Wlku4TwmNWDzaQqzHG+0PSKod5cH5Vh2RiAhYKc8DlV1UPSyuFMGygVlMg1/P6BC6DqXQK8jNZDXAYA1f21V34wMXYFaiyVw0rJyzLgs3VMkxOjGtix/V0XWChZ0cI2i/dzvXdfTd0Qf91BMPrhyNzgKfOmxaWypqaDXHfAgwAtCL8XOfF47gAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-hpp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAehJREFUeNqEUk1v00AUHK/XKf1yZdESVRBXjRSRFqMQVBA5Ic5I3DnwE/gpnLnyG3LgXglx4UDDLZS0RWkDLiRxSusk9u6GXSembmLgWZbX7+2bnZl92mg0goo3b3ffO/ncdvyfjHef6q2Dlvs8Q2ktzr16+SL60jhhZ69bO8X8ClLC7w9XdKJVG8fuM0r1WrJG4gXjgqU1D0MGc2kBTytl+7a9XmWcl1IB/hZKEhccq5aJJ/e3bTu7Wg1CVo7rNLlRhUh4oMnXoDoyhoHGyWmUe+QUbELIa7W8CjAFlMzdzeckCwFN06ATAn8QmDMMMGlMuwWucpoCHNe4jBkAMenjYvRPTyi53JvuwX8AplleAeBcRFrH6rXIxLim9I/pi3QA1RhKaYxdjkN8IwalCMIwWs9ljMkh0wzk+9M7w179C3LZNXxve2h+c3Hu91HeKmD/6zHOLnw83ilB1/V0CeqU3Q81LC/O41b2Btx2N2JVP2riR8eTUxmi0TzBwrKZMsqMoz8MsDh/DWuWhUBKURLKxQIeOMWoptYPnS1c+INZBkwISomOSsmBZS7B+3WOzZvrKGzkMAiGqNy7g+LmRkRfekBnANy2163PZXrSbrQ6vch19Xz8fPDHyL39QzkHBKedXjfu+y3AAGU37INBJto1AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-html { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmBJREFUeNqEUktPE1EU/mY605a+hhZTBNKRDApNrWIRA4nEBUZdmCgLNi4MK5f+FNdu3bFv1J1EXODCR1JJSMTwpqUP6NiCpe10Zjz3hj5Mm3iSybl37jnf+c53jmDbNpi9eb+6Ftcisea909bWNzNb6dwzSXKkhIt/r14+515qBqmDA8HpqKagh53XaopblpIbe+knDpFAhPab2Dw0TKvRK7lmNODzePBgZlK9oUWSpmVNdpIU8T+jaMsyMaD4MDcZVa+NhJMN00w0n6V2nN3yQgdHWZag+LzYPTomIAtT0THVtPGanmb/BbjwLFkvn2IttYGYplKyDzsHh7gdmyAWfh5zVq0Guhg4RAHFUhmfvq3j134aXo8bd+ITnMFOOovU5jbGRoZwNxFn1cxuAIcDW/sZDjA/c4u+BNxOJyxqaenpI3z88gMfPn9Hv98HQZS6RazW6kjExvFi8TGdDSy/W0Emf4LS6R8sv11BmfzSwkPcm74Jo9Ei0GZgmkw8QCOao8OXcaz/5vSZnPdnp3ApqBBLkWJE0Ci7ASzbIhCLLQ1E0iOkBDh9NpUgiUejo8oNuJwyn0YPABtn51UYFFivG3yBGCNZkuDtc/MW+ZQI3OrYpBaARCKufk3B5XIiWyhiL5ODp8+FfFHH+KiKSqWKUL8fC/NznGlPBmz+24dZjKnD0CJDcMoyW0SqXuMtHBFw7rhIAD1ErNUNafxKBNevapwu65NpEQ4FqXIA+RMd6VwBP3cPSERb6gLIFIq61+UqGWaFdcrVt/lmAuWjAi2aiMFwmOYuIJ/N6M28vwIMAMoNDyg4rcU9AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ics { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhRJREFUeNqEUkFPE0EU/mZ2dra7bLNpi2AxQFKalkJrohICiYkXPagXrx78Df4K48GDBzmQePLMhUODNxQ5ciEkJVqDtJGmMWrCATRbd2ecoS5u3aovmezsvu9973vfPiKlhI4XL7c2r5YL81LIELEghLA3u/udxmHnPmfGW/Wuv+LpwwdneRYBx7PeWK0wOYYhcXxyckGV1fdbnbuMsXcklqPRJQxFMKz4RxDCtVO4s3xlRjWoB0FYjlQPEEBieChwKCRGMx5uLtaKs1P5ei8IKlGa/YkXMXYtlTEDlsnw/mMXhBJcqxSK6vlcpa4PEpCooUyIqs5M6hG1o2CUwqA091cFcYLf/sjzcX75EiQIojI9779CTYR4jwTBf+r7GAwh0AxCiL6JMT/04vQ79u8aI2O/7Jzg69o6Go8ewycUahtBpADhHKLnK/eVbkMdtROWIv80NQ2sPhncA9Htwn+9hZG0rY6DzFwJl+7dhs0ZstUy8rduwPS/wd/ehmi3kwq4zTHiWUgXp+EuL8FvNvFl5Rn4xAS86iyI2kY3n0Mv48ByrOQmancdi8I0Kcj3U5iuA29xAelKCUHrEIayzltagG2E4IwkFaQgSC6lYI09iN0d8It5uNV5nG5sgJdKYC0G8WoTOZvBISFNEBxnsuzD3GX4vfDsszzqAu0jkJQDedCGbB6AWg54pYbPo+NGVPdTgAEAqQq70PytIL0AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-iso { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjlJREFUeNp0kstrU0EUxr/k5qbJzdPYpGkpsUJoA2q1oLjTdiGiIC5cuXHlxv9BEOrStTvBnQvRrSAIsejCrlqpsURq2hCJNQ+TNLm5uc/x3MmzJh34mDNnvvnNzOE4GGOwx8+t9XQkfn0VE0Y5/7Z+kHm+dvOhtd3P9c/xwNZh7nWaMYtNUmX/Fct/vlN7/8J5aRRgyzm8xzpRDjGE2aVH4VTqdnoUYg/XkEhmy+Cx3DhA5tMzdFolvg5Mx3Fx9SmH0JIg79Zo3j4GADMIokJTKtjbfAKXU4Y/2NvSfyH75TFOxa9Cmr0XnlPFl5ReOQ6wNMDsoFX6AElqQlNV1KsOuNwS/AGFjEUIDhmn5+/DMM16/9igBowAzFKIswPJr6MjlxFP3sV04gaP7RzMPe6xvWM1gNUBM2UKYlBau3QghGphg29J3gDlLLilWNdD3gkvIIDRhD9yGe2mCV0V4HFXuCxT5Dlv8Dz3sIkAs03FalDxBMQSt9BRBMhNncuO7dyU28c9tnf8C/Q0ZtR4GImeQSj8APLRH772BWcgiFODffCv/t8H9tO0v3RjV7VqkeeXLlzDfvYjj88uXhl4JwIsrYxmLY/M1gYclIvGE9jZfNPrSCD3/QgLyeWTADV6wW9AryIcCkB0u1Aq/oCPumlufoF72vIheaLDr4wCLIOqrYnULA14PSoqpSJEAUilZrD77Sv3LK+cI0+Be8cAbbmAOrob0agtD491LYfkoqvnyZLsWRkA/gkwABL4S3L78XYyAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-java { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAjxJREFUeNp8U01v00AUnNiOEyepQyhQobRBSlVIoRCBEPTAjQsSEneE+An8FM5cuXLNoQduIAE3qopKNJAIIppA2jrOR93aa6/N8yZuUxyxkrXr3ffmzczbTQRBgHC83nj3ca28dD36nx6fvnzrNNrdp4oibyUmey9fPBezEgWVFuYLdyvlPGaMY4fl1aRS+9pqP5ElAkmcnknRwuO+Nyt5u/ETYfyj9WrpZnmpxn2/Ok1Swn/GvtnH5k4TLue4kNfxoFoprRQv1TzOb8cAIu3+ZD7oD/Hm7XuxzqRUNDtdkuLiTmW5tFxceBXlnXgQTAORSMt2oGezUJJJrK9dFWdEH7Ik4dB29LiESeUEJXd7/dAT3L+1ivlCHr8NEzutXTBvbJPPSdO/AH5wysChwM/1HzCGlmAzOrKxu2eCud6Z2Jke2MwThpUXL6Nn2ZAVFTlNw70bK0iRnGAq9qwHtOmTRpsx1NsHyKRVnNPnoMoK9kc2BjbD4vk5JGV5NkBoEPM4FFnCteJFWOS4ntHEfphQyKaFTWFLw704AJ26ZFx/ZEEi3YyY0O1Dmr4EKTUHA8hUnS6siI0DEHLYog+b28RCRuNXR/iQUpPUEQ+NVht6Lodnjx+GXYgDSFRnq97Ed2pXSlXhUSeGhxYc5sKlNXM5DGLR2TMwfZVPAIi+otGNWy1fEZUKeo4qc4ysI+F8VksLIJfYcD9QYgB/DNPMptWBlsnBIS86xmDMTBo/PWd0LB6VZfdEbJT3V4ABAA5HIzlv9dtdAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-jpeg, -.ipfs-jpg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8luUlEY/s4dmMpkWxRopGJNNbiwhk1tItbGtXHr0hcwmvgOdWld6Bu4coXumtREE3ZKu8FgOlC1kIoXtC3jPfdc/8PUIpzkBM7wf+f/hsts24YczuerGUc0moBlYTAYA+i8sbdXtAzjITRtq39kr73s/Gr9DTUYPOeamwvYnHdrdR0SnDebuCbswJGqpX+Uf92Hqm7hzFAG/4TgNr1uCwEJ0trcBC8U0Kb1/PQkHt9JxSLnL6TB+Y2zAIMOJBGLXmtsbEAYBsx8HnqCGKVScAX8uHf5EpqmGXv18VO6VDEe0PXsKABN8+AAgiabmYFNNJTDQ2RUFc8+Z9G0OPR4PKYwvKari0MAgiY/OQGCAajhMNR4nDZMaInrKBGl70SPMScck1NQG3X/CAWLE3/dAWV5hRRVIJxOWNksrP19sFgMqqAebUGYHMI6teq0A9oTVAhqu2sfbYYjsL7lCZ3683gA70T3TK7/B4BNoO020GwB9TpwfAz8LgMtWn/NkV8EHgoB81c7nYwCyBZlEVkHcqMTKFnkmehJTOPvEfCnKi0fAyADJKfXC/h83TaZTJjaa5lANLpOFqAXtlEAorAwO9u5syT5UxLfU0e3o1FMu1x4u7ODYq02BKAMAVSrSNLrK1MhLPj8mNF0vFm+C1ZvwKBwXXE4AGn1WAASazESwUW3BzUSMeJ2o1Aq4sPurvQYSRLwlhRR6mSaYyi0WlpAJrFRx3ouh5/lMt5lv8BLwXp0M4lSpYL17e2uK5wP6lj/c2ZPn2RI+YT8fDvqoyegVLyfG5kBKaQQOfvF2pLc+ifAABiQH3PEc1i/AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-js { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RUQ5ODY5Q0NGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RUQ5ODY5Q0RGMTE4MTFFMTlDRjlDN0VBQTY3QTk0MTEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpFRDk4NjlDQUYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpFRDk4NjlDQkYxMTgxMUUxOUNGOUM3RUFBNjdBOTQxMSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PoT8zQ8AAAJdSURBVHjadFNbTxNREP52t7S0bktbKFAvTUVaw60YqkExUTD6oD74qC/yD/wp/gh885XEEI0RAyYQUiMpIBGMkYR6o23abi+73e2uc04v1LROMtnZPTPffvPNHMGyLDB7sbJ2ciUSli3U35smkK9t7x9v7n2dD/g8KUkUwWqeP3vKz23NxJGzgwOx0RC6mSgIo+WKuvP56MeUzy2nJEk8PWsGJVVTuhWbpgmHw47FB7d98Wg4mVWK52o1sxOg3Va3PmFp+Q2PdUquaFUM9/vw+O6cP3bxwm46Xwh1ALR3/vL1e+hGjcc9koScUsTSq3coVDQsXJ3wzo5HEs3clgZNMTVdx1T0Ep7cn6//QRQwMhzA6uZHLD5cIFEFSKIU+G8LK+tb0KsGZKcTJoEyP08AbpcLy6sbPKdQrigdAGaDwWxsDH1uGbliCYIgcM8WFPg8Mq5Pjzdyu4jYbCE44EepXMHuwXe+A8x3KKYxYsjvbUzmlPGpBmYdgI1oYjSMbL4Ao1YXMkcM2Dd2xnbAamPQAqg1GORLZdycmYTdJqFKk2DPR3fmwI4zBDrg9RADqxPAbPBif2WTSB584/3/TGegEOit+DRcvQ4OZJi1LgwIQKVCg2i6nb1I7H3Br3QWqT9pBAP9uDY5xjdSM3RqxeoUkfVnEOW8UkLykERTNXjkM7h3Iw6NNvHw6JjuhAhVrba0+QeALozcI9nQR0VvNxJc/ZmxCNGvIBQcpDG6udA22kyW29HC72wu8yG579ZoiSYuR/ly2+y9CA4NceWLmo717T1i5ULqJNtapL8CDACskxPFZRxLwQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-key { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlZJREFUeNpsU11PE0EUPbM7u/2AtJUWU6qiiSYYo5EmmPDCD9AH46sx8cEnja/+CB989z+Y+MKPgMiDsYQACcbaWBBogYD92t2Zud7ZlQZsbzKZ3bl3zj3n3IwgItjYeDO3MlWme0bjUth8e8/fO2tHzx3XqUEk50uft+Ndnhdmc3SlfNPkVZT8Cy600DoIISvVfKYtlvfX1p66XmoIYsMZdjJQWvEFbbsC/S5g2QhSkKUK7rx6OzvzqLpsovAhaAxA3DUBQn2TUFsl7KwTfm4Z9DoO5LW7uPXi9Wxpfn7ZKF09vyPxX2iWcNRkKGZz0mQWKoNs8AVB6x1yRY2pYnc2LLofuXTxMgAlmlXIfngCxNxEzM+DPv6NQa2BygLgZyX6JT83ngHTN5GAL0WSoUQkSQnXkyBh/k0GegTAaldM20sTKvet+yyhIZApECamL0jUSe3oFChx3TopM4TeEQP2gc6BgGIwb4KGNXRhCkMGxgg2kJeybRiZM45D8W61qEAknSmpHStBhywu0nFVupSCTAcM4ECwqapv+NQ6LS9JGALoMIIoPYDjZiEL1xHtbyO39AQUDaA7R1AH23DSeSA4hv5RG/VAhxomPYP8sw9A4TaC9iHkjUWmrtGvbyC18BLe3GP0m3WW4I5hEBEnPIStXzyuFIxb4EkMEJ79Qa/xHbKxCdM7xeCwzUZOjgEwnuzt7qLz6T3cySmQP43uzjeIiTJM6io6W19B/NLCKMVGCzkCoLR/0lrfOI2fNy/huKC1FTsK/rbGNeMRC8dHpHByfu+vAAMAL/0jvAVZQl0AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-less { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RjZERjZENTJGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RjZERjZENTNGMTE4MTFFMUIwOEVERjQ5MTZEMkVBREUiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGNkRGNkQ1MEYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGNkRGNkQ1MUYxMTgxMUUxQjA4RURGNDkxNkQyRUFERSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pl1w97IAAAJhSURBVHjahJNLbxJRFMf/wPAIMIxMkUI7tS0VYqlGDLGhjdKkqyZ24cJFN925de+XcONHaHRj4k7TND6SGo1VWwmp2kSLhlqMDbQ87gzPYcY7k4GgoJ6bmdw598zvnvM/95pUVYVma+svcovx8yMnFZHAMJPJBJfDzq5vpX6+/vD5qo/z7DOMBdo/d26t6jFMJ3iY51jBz4M+LP6wxEw40Gy23qYzB3HO7fpmpZCOmfEfa7Xb4NxOrC4lvbPToe2yKE3K1PdPwNOtHdx79ESfq4qKkijB5/XgevIyHxEC24USmewDqD2ABxubaLRkfW6zMqjWGlh7/ByyAtxYnOPnL0Q2+gGGmKRaw8zUBJaTiS5QOO1FJnuIAM8hciaIWHgi8NcSNt+loVDY8JBXh2ojJAR1HbTSNFMUpV8Dxcjg0nSYBrtBxdLbqI1iheCUh9XXNGurAwCdEkb9QyBSFam9TDfoPZ1LUg1BH28IiwEARTVAQOzcFKRaHZpLoa9avY6L1Gfs0c32t4PU6W2lWsV8LAorw0Cs1nXftYWE3qZGqwWHzYp2zzlgetuolVFvtiDLbRRKFTAWCxx2G/KlMtXFhWPqOzsWHJwBx7rxKv2R7mwFz3lw9/5DLC/M4Us2RwV0g3U58XJnF7dvrsBOoX0Abbej/DFKRMKI30fTVGC32WA2m5H9cQQvhYi0vE/7Wdgczn6ARA9QPBrBszcp/XvpyqxebzQ0Tlsq6llxLhe9bD4cFMr9XdjLHpLv+SLGBYHAYiVu1kNOpAaRTWbCejgiw0zGhFGSK1aw+zXbvfK/BBgAPwADAs5GpGsAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-logo { - background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 553 235.3'%3E%3Cdefs%3E%3C/defs%3E%3Cpath fill='%23ffffff' d='M239 63h17.8v105H239V63zm35.6 0h36.3c7.9 0 14.5.9 19.6 2.6s9.2 4.1 12.1 7.1a24.45 24.45 0 0 1 6.2 10.2 40.75 40.75 0 0 1 1.8 12.1 45.69 45.69 0 0 1-1.8 12.9 26.58 26.58 0 0 1-6.2 10.8 30.59 30.59 0 0 1-12.1 7.3c-5.1 1.8-11.5 2.7-19.3 2.7h-19.1V168h-17.5V63zm36.2 51a38.37 38.37 0 0 0 11.1-1.3 16.3 16.3 0 0 0 6.8-3.7 13.34 13.34 0 0 0 3.5-5.8 29.75 29.75 0 0 0 1-7.6 25.68 25.68 0 0 0-1-7.7 12 12 0 0 0-3.6-5.5 17.15 17.15 0 0 0-6.9-3.4 41.58 41.58 0 0 0-10.9-1.2h-18.5V114h18.5zm119.9-51v15.3h-49.2V108h46.3v15.4h-46.3V168h-17.8V63h67zm26.2 72.9c.8 6.9 3.3 11.9 7.4 15s10.4 4.7 18.6 4.7a32.61 32.61 0 0 0 10.1-1.3 20.52 20.52 0 0 0 6.6-3.5 12 12 0 0 0 3.5-5.2 19.08 19.08 0 0 0 1-6.4 16.14 16.14 0 0 0-.7-4.9 12.87 12.87 0 0 0-2.6-4.5 16.59 16.59 0 0 0-5.1-3.6 35 35 0 0 0-8.2-2.4l-13.4-2.5a89.76 89.76 0 0 1-14.1-3.7 33.51 33.51 0 0 1-10.4-5.8 22.28 22.28 0 0 1-6.3-8.8 34.1 34.1 0 0 1-2.1-12.7 26 26 0 0 1 11.3-22.4 36.35 36.35 0 0 1 12.6-5.6 65.89 65.89 0 0 1 15.8-1.8c7.2 0 13.3.8 18.2 2.5a34.46 34.46 0 0 1 11.9 6.5 28.21 28.21 0 0 1 6.9 9.3 42.1 42.1 0 0 1 3.2 11l-16.8 2.6c-1.4-5.9-3.7-10.2-7.1-13.1s-8.7-4.3-16.1-4.3a43.9 43.9 0 0 0-10.5 1.1 19.47 19.47 0 0 0-6.8 3.1 11.63 11.63 0 0 0-3.7 4.6 14.08 14.08 0 0 0-1.1 5.4c0 4.6 1.2 8 3.7 10.3s6.9 4 13.2 5.3l14.5 2.8c11.1 2.1 19.2 5.6 24.4 10.5s7.8 12.1 7.8 21.4a31.37 31.37 0 0 1-2.4 12.3 25.27 25.27 0 0 1-7.4 9.8 36.58 36.58 0 0 1-12.4 6.6 56 56 0 0 1-17.3 2.4c-13.4 0-24-2.8-31.6-8.5s-11.9-14.4-12.6-26.2h18z'/%3E%3Cpath fill='%23469ea2' d='M30.3 164l84 48.5 84-48.5V67l-84-48.5-84 48.5v97z'/%3E%3Cpath fill='%236acad1' d='M105.7 30.1l-61 35.2a18.19 18.19 0 0 1 0 3.3l60.9 35.2a14.55 14.55 0 0 1 17.3 0l60.9-35.2a18.19 18.19 0 0 1 0-3.3L123 30.1a14.55 14.55 0 0 1-17.3 0zm84 48.2l-61 35.6a14.73 14.73 0 0 1-8.6 15l.1 70a15.57 15.57 0 0 1 2.8 1.6l60.9-35.2a14.73 14.73 0 0 1 8.6-15V79.9a20 20 0 0 1-2.8-1.6zm-150.8.4a15.57 15.57 0 0 1-2.8 1.6v70.4a14.38 14.38 0 0 1 8.6 15l60.9 35.2a15.57 15.57 0 0 1 2.8-1.6v-70.4a14.38 14.38 0 0 1-8.6-15L38.9 78.7z'/%3E%3Cpath fill='%23469ea2' d='M114.3 29l75.1 43.4v86.7l-75.1 43.4-75.1-43.4V72.3L114.3 29m0-10.3l-84 48.5v97l84 48.5 84-48.5v-97l-84-48.5z'/%3E%3Cpath fill='%23469ea2' d='M114.9 132h-1.2A15.66 15.66 0 0 1 98 116.3v-1.2a15.66 15.66 0 0 1 15.7-15.7h1.2a15.66 15.66 0 0 1 15.7 15.7v1.2a15.66 15.66 0 0 1-15.7 15.7zm0 64.5h-1.2a15.65 15.65 0 0 0-13.7 8l14.3 8.2 14.3-8.2a15.65 15.65 0 0 0-13.7-8zm83.5-48.5h-.6a15.66 15.66 0 0 0-15.7 15.7v1.2a15.13 15.13 0 0 0 2 7.6l14.3-8.3V148zm-14.3-89a15.4 15.4 0 0 0-2 7.6v1.2a15.66 15.66 0 0 0 15.7 15.7h.6V67.2L184.1 59zm-69.8-40.3L100 26.9a15.73 15.73 0 0 0 13.7 8.1h1.2a15.65 15.65 0 0 0 13.7-8l-14.3-8.3zM44.6 58.9l-14.3 8.3v16.3h.6a15.66 15.66 0 0 0 15.7-15.7v-1.2a16.63 16.63 0 0 0-2-7.7zM30.9 148h-.6v16.2l14.3 8.3a15.4 15.4 0 0 0 2-7.6v-1.2A15.66 15.66 0 0 0 30.9 148z'/%3E%3Cpath fill='%23083b54' fill-opacity='0.15' d='M114.3 213.2v-97.1l-84-48.5v97.1z'/%3E%3Cpath fill='%23083b54' fill-opacity='0.05' d='M198.4 163.8v-97l-84 48.5v97.1z'/%3E%3C/svg%3E%0A"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mid { - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mkv { - background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M7.5 91.1V71.2h6.1l3.6 13.5 3.6-13.5h6.1V91h-3.8V75.4l-4 15.6h-3.9l-4-15.6V91H7.5zm23.5 0V71.2h4V80l8.2-8.8h5.4L41.1 79l8 12.1h-5.2l-5.5-9.3-3.4 3.3v6h-4zm25.2 0L49 71.3h4.4L58.5 86l4.9-14.7h4.3l-7.2 19.8h-4.3z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mov { - background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M6.1 91.1V71.2h6.1l3.6 13.5 3.6-13.5h6.1V91h-3.8V75.4l-4 15.6h-3.9l-4-15.6V91H6.1zm22.6-9.8c0-2 .3-3.7.9-5.1.5-1 1.1-1.9 1.9-2.7.8-.8 1.7-1.4 2.6-1.8 1.2-.5 2.7-.8 4.3-.8 3 0 5.3.9 7.1 2.7 1.8 1.8 2.7 4.3 2.7 7.6 0 3.2-.9 5.7-2.6 7.5-1.8 1.8-4.1 2.7-7.1 2.7s-5.4-.9-7.1-2.7c-1.8-1.8-2.7-4.3-2.7-7.4zm4.1-.2c0 2.2.5 4 1.6 5.1 1 1.2 2.4 1.7 4 1.7s2.9-.6 4-1.7c1-1.2 1.6-2.9 1.6-5.2 0-2.3-.5-4-1.5-5.1-1-1.1-2.3-1.7-4-1.7s-3 .6-4 1.7c-1.1 1.2-1.7 3-1.7 5.2zm23.6 10l-7.2-19.8h4.4L58.7 86l4.9-14.7h4.3l-7.2 19.8h-4.3z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mp3 { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra7XWxpSsFYIbVQf9REFBHkYBRIPJh4wrN3DsZ4MPGP8b/wUCIHEw5EY0w04o9ILcREGmwVgaXbbXdnd2bXNxPahGyczebtzrz3ve99740WRRHkWn5cebu4cH6SMY7e0jRAHr9c3WxsVvcemmbys9yT6+uHJ8oaPefypdPDD5Ymh5w26wMkEho8JtDtuEOZFCrvN/4uJZNGH0T59D58X/C27aFNAL3Xthmsww5GCyN4+uzu+OLtQsUPxPQx6ZMAoQjBAw7O+bEVCMMQgqygs+LFs1h+dGd8bna0QmXO9OL6JYgwAvOFZKKoy3V44CgNfv7Yx8oLH+lUEgvzF8Ydhz+n41snAGRG5gUEwClzhHdvttFxfNyYK0EnJozKK5eGcf1qHo1GOxtjwI+pfvm4g/W1qtJgerYE2SXJSIL9+W0jk0mCShAxDXgQKgbNXxZq35vQKCiKQkSUXdc1+gcch1FHGPmKuIgBCdc66qJQHMG9+1NIpUylxxHtuW6gEiTIu+N4yjdWgty0yTmdNjFzcwKjY0MU7MLt+IjoSad16FoIx3b/A0DZ7FYXnsdpAjUMDOjI5zPgfoBsRodhhGhZHfBBU/nGAGRtxWIOg5lT2NtrI5dL0SB5KJzLodloqXaOEatPGztKq5gG3S5DNjuAK5NjKJfPYKI0okBkSdemCiSgS/rkQNLSePtxBj4LSCwfFtE0krqqX7ZVMnu9XlMXy2l7ME0dzA3iANQyY6vWxC61UY41zTyNcYh6/QCNXQvzi5dR39nHVq1BUyuMGAARsF6tbbe4iKD1r7Om5iFBdmW1SsDflLiuB6sX90+AAQDHAW7dW0YnzgAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mp4 { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnBJREFUeNpsk99r01AUx79psrTrujVtbceabnZs4DYRHSoMh6Dgq77rn+AfoA/+If4Bok+C0CfxVRDBh+I2NqZzrpS1DVvbtU3SJPcm8SSlsJlecsn9dT73nO85V/B9H0H78OLdt/LDlQ1uMYybIAgI9n99OWxoe83nkiz9hDDae330JvxL48O51Xxm/enNtKPbVwAh0Ec6kYpXat9Pnl2GBC02HrjM5Y7h4P8+7FtIFVJ49OrxUnl7ucIdfhv+BIDv+fBcj7p/tXMPrs2RXVTw4OX2UnFTrXCbbY7tpMsA13FDSDAOQ4gJEGUJLs0PPh9CkESsPrmxxEz2lra3rnpAt3G6adgdQhBpmeLkFodNmsjpOPoXBrQTDcmFFNS7i3MRDzzPCw/vva8ikU+COQxm14BBhvJcHLGpGPTOAJxxeLbrRgAkYujBdH4G5oWJWXUW19YL4XqunAMFhnq1BqWYgaY1MAHASQOiU96zKzkU76mwehaOvx6h9uMv7KFN3RopL4oTAI4HRh4wSl399xla+00YbR3yrIzM9SzSqgJJnoKcklGrH08CcJjnBtLLCsSEGGpSWJvHtDKNoFippsJ0ulIsDDUCCATMlBQkNuahEyiZTcLsmFBKaQxaOk53TlHeKkM70AjAooCghBOk9sKtIvqtPqS4FBaRnJSRX8tj2DOh3lFB5Qw2ZNFK5LRo6w4sKt2ggAzywidAMN/9uIPSZglBLDO5FF3mRD3wHE9qVRvoHrUpfn+UEQK0/7ShtwboHJ6jdH8RZxSC57hSVETb7e5/2u0FxqPHJow+8iZ4lYY2QGu3idhIxO7Y7p8AAwALCGZKEPBGCgAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-mpg { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNpsU0tPE1EU/ubRdlqmnUBboa0UeUQDiUGCC1+JmrhxoXt/gBvXJi74If4AV0Y3sNKF0YUaICqoIfjgVShEiGF4tDOdO/fOeOaSKtie5GZu7pzz3e/c7ztKGIaI4vn9p+/P3h4e4a6Pv6EoQBDiy7P5rc1P1Xt6XP8M5ejXo6UJ+dWbuemeTGdpvNdiNe9YvQLe4Bi4PmTpRmyq8m71rp74BxKF2twIHvAo+f/l1T2Yp0zceHizfOZa/xRnfBRhG4CQqAYioBWeXDyA8Di6ei1ceXC1XBwrTXHPH2vW6ccBBBMI6BsSUEQzakGL6xB0tvjyBxRNxdCtc2Xf8R9TyaWTDOg2TjfVdw6hqIoE9B2GxkEDWlLH7s4ette2kSp0oDRezrQwCIIA3oGHr0/mKMmE53qo23W4+w5S+Q5ohob9X3tgHgO8ULQACC7gMx9mKQP30EW6mEHpYi8xcJEdzMucjfkKcrTfmqmiFYBxCF/Id+gayKJwoQjHdrA5v4HK7Cq44KjZNWpagaqp7QACks0H9znW365ia24DzoEDozOJbH8eVtGShXHTwNracnsG7q6LzsEuaAlNPm9h7DSSVjLyCMkppDI+GS2StQWA1RlKo0X56n2X+6QHkmkDakxF9WMVqWyK+s/BrthYfvWz1Ug+zUDcjMPMm0h3pxEjFma3CbIuCud7oMc0LL1ZgmElpGJtW3B+15HIGNITrMYIlOH7i0U41NrInREylYbu4R5qQbQBaAh95fVKZCnpQCnb9DrWZyrRERS6NDeUw+yHaXh7rt4C4B8y+9vkwn7kwKNRpDoa9aiFKBYnF+RcREqQ2e1m3R8BBgAy9kz9ysCE6QAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-odf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAi5JREFUeNp0UktrU0EU/mbu3FfE1KRRUpWYheALNBURUVy7cy9UkO6KW/+Lbt0IPsFui4gLBbUqFaUuXETUKCYa0jS5yZ2ZO557b5MmTXpgmDPnfOc7jznMGINYPi0de5UvmpORxpjE/kbNqW005DVu8TWw1H758ZfkFgNgJmtyxSPRjJIj0QTW/RDiYGXGb7Dl32/eXrVsd0gSCx9miqC0ooCdp69g5Q/h6OLN0ty5ynIkwzMwUwh2FwMdcbDiCZQXlkqFCpEoPT/wih1YjLInANcD+/Ua9bu3wJlGvrBZCmet2+S6ME5g4oGlZ9A/I70XCDhhDexPNTFmswJBwcnuXkF86VSNZxVu0ukLSGnBcqlnN4HoCQIaIuIv7LUooMOgQ7q75LAAb59B9gCBHSKgqemRr94mMKmD24CfM8nb7THYGQNLpAkUkcb66JyGBFFEWRVL57gFEH5qj8Lxwca2qS3EZaugmzAw24dR/XQgwtsCSBjPIdWbUoE2UJLBnV8Ac/ciWHsK9/glWLnD6K2vgPszsOdOQdfeQ1c/ThKoTgDn9A3KUED/52d45xchZsvorD6Bf/Z60riV3Q9Z/0bbGU1uopYGkfERSQ3VbsMwl0qlqoIARmSoPYXWy0dor79LfBMEEd8jGs/uQ3Yl7PJFNFbuEXiV2riCf88fovXhBbo/vqP3t02/ZYmJFqTkzY160Go9uEMbFK8hR/NrdXtFuUVmnmySVGgO4v4LMAAjRgmO+SJJiQAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ods { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAetJREFUeNqMUj1IHEEU/i7u7Z23e8tGgneGQPw3hZDkkhQiSuwMQREba4uUgpVlCrvEQhurkCoWqcQQ0oTAaYKNqJygGEwgHCSB6Knn7eXcdX/GmdHVPWYFP3gw78173/vmvYkQQsAwNvckq96UnyIEh7/d4t7uUd/8y+85P+bXSX4grkhI6nJYPW7LrXpBK2YxiSoShhu4Buq1NPofDeqdrZ3Z4cl7D4J3UtA5VyVAlmJoru9Af2ZAp1lcCQ3nqgiuKmbY3l/BH+MnHM9GVLP0Ww3KNA33CQoQQnL834Fj74PUGkANEIkCSSsa8gQqgYTIcB0PVsXB318GInRiCVWCkpRFAs+j5gKlA4t29Ggh4d0t04FKt9PQqF4UFgumSEA8ApeaElilWbYRVy/lsns/N1QBkxtENF4jxPxcgcB1CZVOrvMteK5IQDtJJIGh++PcX9iYwWjXK37+vP0WdYk0Ht99jtX8JywWFkQChw4tc+cZcvlF7rMze+ubbxN40fMalRMDP/6twaiUeK7wlZ0TD0a5hLTWxo2d45KKprqHKJslTsy209s2wnMFBTYNZjc/oLt9gPvLOx+hxVJIKS2YW5pCbSyJTGMK775O8VyBwDJd2LTDl/X5i8v3S7NVw9vJb51tITDEUwEGANCx2/rXEEFFAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-odt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAepJREFUeNqMkz1II1EQx/+7Ca6JkqyYiJ8cKEpAQbBQFDm0sVOsFBS9wt5KOTgEG5twxVlZ+XEnKNiIghYKxx5nwEpIIXaiSAgKGmMi0d23u8+3T7OaZJEMLG9mmPnN/w1vBUopLPNNhRWXHOyDg0nx82TiJtZPlPVoNpftc2cTotcHtxx06kdXpSQ/BvzKESZzIDmAz6y+NojOjpDMZiqRPIgNoFyWM8DrKUV7axO+gcp4g7AzmquAdVNqOgL2z2I4id1B0wgeygOyt/rLL5buLwAIDgA9dY+L+DkuDQOCrkMgBsRglcMOqAGwIstMg8AkGsuZMNUMRMkLqE+QGloglvlA7uIOAKvZajR0qJkUj/XHe0BTIclVKKlrfKsj9qA8gA6wqSJzPaXlr7ky//tdLEUfawsBjExUFGVWbT7AxSa42H2LMfODmvd3wKb7RAMLYwM8nts8xJ/pEe7/3PmP2eGv3D+9usb35W0bINoA7RmjXSHsH0f5Z/mUSZ0Ir2JmsBtD80s8/rGyzWsLFTD5yUQCbfUBHl9d38LvkdDTXIuHVBo0k+bbt06qO+yAPGXwe/cA4wO9PN44jKDG70GougIzi2tQ00ms7/3lpwnBBgjZ37Kkd1Shht5XzBIFl/ufFtniT/lFgAEAU//g6kvdGBMAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-otp { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAcJJREFUeNqMkssvA1EUxr+ZjkdbrfFKVD12ErYSRELY2fkH+BMsLcQaSwsrSzZi47EjJEQkEhYkFlhYSVtFpdqOqpk717l3jKZmiC+5mZlzv/s795wzCuccQncz3YeRBj4KHz0/RrOZe2NsZPP20o255zQ3EAxzEAC+6uzTw13G4TFQAakA/CWtIYbY0KBOrx7IvwDQqlHV1o3YxKTOvyAUvfQCfqmA3e4ikyS/zRAKvOot7eoSHEgZIHrCfQAfBqBaKQQDKScQAExd8emBANg+2U2CvNMkkgSqBmrCxFB8mujeoJBWwEqARcssKTAJEGrmaGrjqK1zvNknH4BtyxKl2VUpRxmj5W+x73q9AEaZrR/ND1EJluIpS3i9JQiA+a+hSq8HwJjTsLrRaWitPTCOlhEZn5N75sM1qigmlN+dB3u++Qao5W4TtbEXXIsiszGL4PA00itTsu6XnQWo0TjMTAJqfMDx/ryBJcaVzSNSH4fW0Q+rkIf5rsjRiid7yyN7uoXS3Zn0egE0NiORAN9bQ017D1Lri7CLlP2EDr3Rf7C/itzV2bfXA/igLDaRixfngFhSCooH2xVPCWBlwKcAAwBX1suA6te+hAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ots { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAfZJREFUeNqMUk1rE1EUPS8zmabJdDKB2glEwY9ExJYiBUEQpV25qgtBXfgbpEtXuujKf+AfEKRddOdOGHClbYVCvyKWaijT2mhjphk7Sd7Me76ZONp0EsiBYWbOvfe88+69hHOOAE9f3zTVnDKNHvhlsfqPw/rM0ovyWsRFdXJEpDIyRnSlVz0KSkmvabaJeXSJBEhgAJzTDNybmtUnS5Pmg/lrN07H5NM/f13FoMgpXDSuhiIiK3Qi6LUugX7FAbaPPsJqfIHHKCStqRsXVFPQuZgD9BBxjikSiRq41AAkgCQBzVf0+BWEBX7GBm0xgHHUqk1UbBuEcIydzyCZlOI9YEGuDxwduCCitS3Xh3viCZ4jrcq4PJ6DLHd67tjtuAAXib54dCPVEfQ5XIcik/0/2iDeOYz3ceCxrisMi904y0XiMQFfkB7lg6xFHwFxEqUMV0anUNBLWKm8xd3i4zBWOzmASx0UsiW831mA59Xjm+h7HCOygduXHqJatzA7Poey9QnXjTuoVD/j/sRcmDOWLgqnLC5A2wwST+Pn8T629lahSCo291bwu9XA7vcy3m2+gTaUR14thrk9BXasbdiOjSe3nmPpwys0xSi/HpbDd3bIQC6dx/q3ZbRb/j8BEi3Po5cTJpHI9CBNDEa++GyDBN9/BBgAwfDlCVUQaNAAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ott { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdFJREFUeNqMU89r02AYfpJ0iVm7EqhVOxw7dDBEdpiCE1RoEZRddvUgbIex/Rs7eehppyF4LOzQu4MxwYp0HgShIuwwUVSCVtl0s13afl+SzzcpyZYmyF74eN583/s+PO+PSEIIeJZdrtQVI19Cgmk/Ph39bpllXq82g7sgLxVcyKNZpIx8Uj5u5zSjc9Gov8ZihCRC8D+7On4JczevGeTGSEIC4ctKJtB1DTPXi1iCCEkIm1EFlC2Em0iwtWfinXkIzjiO0jljtDC5TtflGIGUQMB+mfja/oPv2Rx9MMjpMdJxOXyXTwkcwIkewfqQ1QtQNB385zcI14FrtQexsSb6SRysZ4Fbf+F6eHwATc9gJGNAm5iCTL5n/LCVRGADNoeaGoHqyaXj5gqQlTODovcwNk5Aj6wXqV8eCo7EDhMonEHpW+dZC7gUG98D3geo7vkb01h9cAvPdt76OGy1xntUd3bjUxAk3+l2sHJ/FgtrT0MUJNfDSm0bjQ/72Hzxxo+NK+h3B7XRNO4UrwymQtMIkdTBU0m+sBOayLsn8Ka78mQDjx/e87HXPkb1+UsfP37+AmZ1fP/suknBb6nefVQXjl06TxMlJfWKNWr+Kv8TYAAkUueexJF47QAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-pdf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmhJREFUeNp0U0trU0EYPTP35qYxaW6TlDapNKWGbgo2FkF8rARB6rboXusf0F/hyq2U4krFqugqSBeuAyL4SERBstHa0iR9JKZJ7mvu+M0tqZGkH3x8987jzDnnm2FSSqh4ns0VU1ybFzj674Wa3uWiWbfsFQb+jrGj8Xvbm0HlvYVRxhJprpmTlGmum+OMm5uNPZNbtjk3l82ey8++8oW4Jv/H/wdA456g2kvH99FyHNiuAz2dwflbN8YW8zMK5Go/CMfQkAhpGsyQgRCtlpE4jIULyC9fHzu7MPPEl/5ib6WOE0JJNRiHHg6j86mMjw/2gG4bkbY4PW4Yj2j64skA5FTHdaEMPiAJszt1sK0d4suJmY4k0+IDDGRfqmh0u5gejQc+fG8eYCIahRQCEfgQnIuhEkgtONE+dGxYxEDj1DhiEycZ+1YXdUpHCqTMJIYyEES5aXXQsi2kYlGEia5GtHVKn+amPBeCutPgfLALPuVu+xDVPw2EQyFEjHDghbpYNm1yKVVnYjTOerepn4E6XQmLGSPkPkOXWATMSDcjQEkAaqOu6+i/rccALtFL53LI3r0Nq1ZD4/MXZJaWYFer+PXiJc6s3IEgY3+uPYZHTAcAHM+DTE8gnM1CSyaCulv+GrRy8uYyElcu4XfhLVpkpNtn/DGA5Uu0abFH36WnzzCayWAkmYJvWeCkfb9SwY+NDbSoOx4bYqJF8rZqVRRXV/HhzWtUSmWwmWl0RmN4v76OUqGASrmMOkntSHF8MOs954dT08W248wzYsJDOujRBAaqqikTpRo/qqd0/dv97c3Lat9fAQYA4z8bX9nTsb8AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-php { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhNJREFUeNqMkltrE0EUx//ZbDaXNrvZzdIkbYOXGgxYQlCK2IIY6EufxGdB8Av44AdR8AP44JOPBR+Ego0PClUKTTXQSmkTYtOkmubSJrQ1e3H2yJSEJNIDs3PmP+f89pyZcdm2DcdWvn7LzkxFHmCIra7nm9ulg8yLZ09yXON55Dgjt1PM2iPs0+aW/frdh8bzV2/SvQBnCLiEqcFxLKSSodlrU9leiGPihWePBkgeEZO6ShC2dCAZNuf6ADb+ldQ5PUPx4BCFcgXfdwq4Ph1Dtd5CZi4Nw7SQiMdCXkl6yVIy/QBWgcU+yx/XsLK2cdHndqlK/lZxH/OpJO7fnsWY3z/YAq+g0TmHpoUH2vB5PXi8RD9Fo10aAmDJTgWyIuOupmK38rsPcOvqJO33XWEvwLJsmKxHRVEwf/MKWl/yUMf8mIloWN8rw+sP0D6PHQmYuzGNgCRiMZVA17IQV4OIaTI8buH/AJMFd02Tkp05PO4jnWvc57EDAINt7u1X8Pb9KgI+Lxbv3cFR8xjx6AQ+b+Txs/qL9KePlih2CMBCq92hg2qzt1AoV7H5YxdhdqhHzRbgcpFeqdUplpvQW4FhmAixZ/sws4BoWCM/qmsE5XqE3dDQCrqGAYWdejqZgK6GUD8+IV9VghBFN1RZJv3sT5diBwC15gncggCPJKF0WCPN8dun55jQdVpz3Ynl9leAAQAJhiGatD9AOgAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-png { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmtJREFUeNpsU9tOE1EUXXPp0CAUWmJbC04xBANNTF+kKhG8fID6aqL/gPEj9E0lIf6Dj30HL03wxQtVIC0QKrWxNG1Dk9Z2Oj1zxn1m0oIZTnIyZ8/ee+211z5Hsm0bYg29fLGpxWIJWBYGS5IA8ncKhT9Wvf4Yqprtu+w3q85X7f9QxseD/pmZMZsxN9fnc5JNw0ACGGv6tPSvyvEDKEoWZ5Y8OHHObKpucw4B0t3agnl4CJPs2YkQVu4s61ORaBqMJc8CDBiIRhhVM9bXYdVqYAcH8M3NgS0tQQsFcfdKHEbvlr6WyaR/V6uPKPy7B4DT7lUq4MUipMlJ2MPDUKtVfKZ2nn/5BoNbkONxXeb8LYXe/A9AJLNWCxgdhZJagDI9DZg9qIkEytRSkdqTSFQtGILSbgc8LViM+tc0yPfukzIyOJ359k9YR0eQdB2KmBbpwXoM3Dod1SkD+scpEapCI5DdpsJhIJcjajQZagcjI+5oLe4VkeQnyiZgdIH2X6BJ7dSqQLfrggjw0AQwP+/GegCIHppNoFAgEMO1RZKo7BQgRi3yN05cnwdA0BQMAgF3C6pnbuNg92M9AFT1diSCh6kb+FGvo2MxnBB9ocZxp4Mns1cde213B81e7xwAcl4jkaa0IUSjUdLJwkL0Ej6VSvArCt7l81iku6GrKnYEU89VJlSJRmR0Dax+fI9suYxSo4HlWIw6M3FBlnD9YhiXabyOsOeIqG7TzDeIYo6EDGp+ZPb2kKKqH8h+mkxiI5/D1/19J3bwYPvPWXq2skkiJVxesqt0XzghpKM8nRVV2Lv2q9eLIvSfAAMAaacnllcFBmYAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-ppt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAkhJREFUeNpsU11rE0EUPTM7ySZpmzT9DNamWAtFfSiCigr+AxF9zKtv/hvf/Aki+FEi6ov4ItWHPGiwiBUKoUqqTUJImmR3M7Mz3t0kNe1m4LIwc+65595zlxljEJzdR5uf5nLmsvZx6gSvtd9W9bjhF7jg5dH9nRc/wq8YXaTSJptb0xklx7IZoKUEz1zJ2DUU69/37vFYrDxegJ9U0lC+AoIIVGg9CL+vIObP48KDQn7x0sWiVnJrnEDg7KGk+i/Ac4iUM/R7BsmrSSxtXMfa3X7el8+Kjf3KfUJ+iRJQw4w0Tc8BRyWGRAZY3rBR/VlC+XED2ayDhZyXl03+hNA3TxNQshlGLAnE44zCIL1goXZwiMNvB1i6zbC0KuAsxNITWwgNMYPeLVJiFEO9ArjHAivrAjNzBr4f4vwIgdGD4YUACsZCE8AtYGWT5jCsGQw5wEYJzP/pj5RwYTA1b07eQmfZ8P0sgdaM2FlYwWkMgMpl6NQAO33GKM0wsQWflkh1uqGVmVWblsiDkQyqxwfag35SqcktaEWTUTHYNx4iGU/C29+BvX4Lpu/C7zYgFjegSY63WySsHyXwpYHU00ieu0bAOuJbBTArBkiXKiaAmTzcvRJUV9E8rOgqBwqlY8ASs/AadbRLb8CzeTjVClqft6FdB17tL7yeCbFRBYoLr6vR/PiSEl5BZJaBD0/R2nkOZqfQ2fsKt+0SEQ+GLSIEUvJm+6jbah2+pS2aon+4g/afd4SYJVuA7vvXdC/IHQtSoTnK+yfAAIEaId1m+vudAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-psd { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAqxJREFUeNpsU01ME0EYfbtdKKWGtoItRWgJHApCBE2I0YuoiSaaeDJeOJh41YN3TfTixcRwMfEk8eDJGA+Eg0YTTRRMg02KKFooCBbTlkJLS7f7P+u3K9Xo8iWT3Zn55s173/uGM00TVlwZfzJztD92iKO5ouvQGQPHcQDN380vlDPr65fdLj4Oa41i9sFt+ytgN7o7woGOrqgvvpLBaF8vWj1NUAwGTVNRM3mf5vU/zaU+XySQuTqIFXz9hxmGLkoS7r+YxvVnrzGzlgXPDOzUZPT4m3Dt/KlIuH9oUjXYEHZZ/wOgGQZi4TZcGI5hLb+FO++TSOSKcLtcMA0dI0EPrp4+HtnfG5skiUecDGwQE2MjAwiGWlFVNDz+tIyCokJhPKYSX7Gdz2I01hOJdnY9rJ/7UwPGTEiqjtbmJtw4MYx78S/4Wa3h5UoOYwPdIOp2Xi/t18rlFgcDw6o+ydiWVRwOBnCpL0oOAMmNEhLZIgSeoxwGSWcERon/M9DoBknTIdNQNAMnO4PIVGpIFXcwndlA2OtGc4MAxml27p4AIulWSIa9QVadiYSoJxhqBJivKgh5ad3k9gaw6JdlDaqq7q5wINY4F22HaLHSDZQkBW72O9cBYFEviBIURQH7a7MN0uDisUW12ZZcaGlmdq4DwCqeTo1zNtZuW7hUqGIw7MNqSUS2ImNsKEpSdEwt5lGhfQdAkQBEoub3NNrDJfAIeBuRrcrY5xGQ2RFJAjl00I8PCckJUCB9q1URBnk38XEJEuk41tmGwZAf66s1VOh2keqwoUnYpFxHH4iKIixkN3HzVQKP3iQR/5GDKMuYmE3h+fx3MHqh1sMafztHLuiCg0FAk0uFdLqcpGY5QEXbTC/j7mIaVjc18DxufUtBJ/vcggs+3ijVz/0SYABsJHPUtu/OYwAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-py { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAlVJREFUeNpsUktvEmEUPTPzTUFmgJK2UqXQFG3pA6OBLrQxamJcaYwuu3Dp0l9iXLvVtRuDpgt3JIYaTVSaxtRHsJq2xEJBHgXmifebMhECXzKZme+ee+65516h2+2Cn2cb2VwyHl12//vP2/zOQaF4uD7GWN69e/LogfNm7kUsPBFaXYwHMeK0OlpQEJApHJTuykzK98dE98O0bLM/UNgr4v32Dj1fwSQRt9dSsfmZcMa0rIv9ODaqYrPVxuPnL1Cu1aEbJu7fvIZUIo4bqeVYRzcyv/8c3SPYpwECt/dmu4ON3Ed4TymI+hQc1ZqoE+F+uQLDsnHlwkKMscJTgl4eJOi9fxZLePNhGx6ZQRRFqH4VjZaGSv0Y6cQcJLpra0ZguIWegqDiw7lYBBZV6xiGk9DQDLzK5bEyF4Hi9VLMsoYI7J6Es5PjeHjnOl5ubqHaaJGBEkzbxplQAKIgDmBHekDTgI+qKKqKLvNApgmEgyquLs1CoFn2Y4cIeLJpkjoCLkWnUSIF3JxISIUsCjAoxhWNJLBIJs3YeXj/08oYZkOKY65HllE/bkMmY504YUd40HUq2JSSyW6iVPmLiXE/ZMYQCU+hXK3h1toqdNN0sEObyKtqtDQ6kXDwcadDS2TBryp4nX2HxXjsJK6bDnZIAZem6Tp5YMMmicn5OC4lztNWtvB9cg+hQABtWjKL2jH/T3GgBcYDXEE6mcDM6SlaJAGMWkivLBC54ZgniZaDHSI4rNSqn7/t1vgkGJPwZXffSeCjk2iUWz9+nSTQN8e6ef8EGAClUi/qoiOc3wAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-qt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnVJREFUeNpsU8tu00AUPU5sp41NkzRxpfSZqi0VIIQqEEJUZYXECvbwCWxYsuBD+ABUFrDrCnWBQEJdIWigBSr6pqRJ1ebhxrE9M7aZmSrQ4o505fHMnXPPPWdGiaIIYrx89GKpNDdxmXkU3aEoCsT+z8W1Sm21+jCpJctQTvaerj+TX7WbnJ+0cpfuX8mQtn8GgJ4AZtIFY2Hz3foDVRcgyt+cRHcS0IARh+D/8G0PpmVi7smd0dLs+AIjwTVEiANEYYQwCHlEZyJgIQKfoX84g9uPZ0cHZ4YWmE9nuufU0wABCSSImMsWEgqSuoqA/39/swZFTWLy7vQo7dDnfPvWWQa8GuOV3IYLJXmyzDzG2/ChZ3pwbHdQ267BKJoYuj7SF2MQhiF8LuDK/Gf0DKTBKINz1IbTbEMzU1ANDW7LAfEIQKIgBsBFlAx6LYOz6MAcvoDCtAVGGPKlAiIu/F55F33FDA6W93EOAOMaMOl7biKPwRtD8Foetj5sYPfTDtxjl1f3Ubo5jkQieQ4ACSUD2iE4XDpAdbUiW9D7UsiN9WNkZgxajwbd0LGzt3keAJPUc1N5SVeENT0Ao2BKV6QzwlZeRBSKAYhe3aYHcZWn7l1EfjyPypcK9LQGa8qCvW9j9+MvaasQOHaRhGWdhsNLR8hwodYWf6B4tYjDjSOovRqq32rSYq/lytw4A77o1V2ERiAtzY5kkUrrsH+3QF2KY87ArTtQuQ6nAf4x6FCV1D001+vYersBM2vA4y1Rm2D7/Rac/TZIw4d/6MrcGAPf9htN0miJh7Lyuoyvr8rQeP9iVJcrSKgJ+TrFcyYebXTP/RFgAFQobmIOBxbsAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-rar { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnpJREFUeNpsUktPE1EU/u68OgylZXi0hZACQU1LEKKCMcat7jTRnQsXxsQtv4E/4M74P1iriUaNCw1FgxpjCJQKKAU60+m8mJnrmSll4XCTc8+959zz3e88GOcc8aq9evChOHl/lvMoubvWX/z4+BwTlbvw7bXdg8b7h6LE1gGW+O88CRMt4XTlR6/rYxce5Xv3jlHH19fPkBu+gWy5mlcFb3Wn/umeKOEMJF5C7xCFbtA9dRXjFoYKGiTRAlPGUV1aKU9O3VwNQ74A8DQAIZxqAuAhBPIMFYpQVAVB4CPSZjEzv1weH5tbDQN+JQ2Abu488mnzIbAAA3o/VK2PwDJo7r5Fy7ZRuvi4PFS6+qIXdVYD8Jg6BUcuOD8BozSLlRWyicgVKkTMQWwUlFF0Ooe5FIPk57BD7G0SiywyjD8bCDyHsOkeeeR3SUxEkROmU6BfQYFJMHfhWXV8efkUrb13VPMTsrcTQSzxZ/+n0GVA6EGbSGdgG9vo15fg2nFgbO8k70SRdd+mahDT81vUxTZRlJBRMsjq89C0EXCvSf7TIBZ136YZUJEiE7LgJ2dN01BZuE0dkIhxE7KcQTK1QUj+cwAEyrPZ+IydzRoyah+mLy2isbWBweESJEnB9q+1RM9Ub9GQOWkABg8HjRr2d9Yh0hTlBlRsfn+D4vg0BvUC9rZqECUJuk7Tzr1zahCYlB6HJAREPwfbbMBzLBzsbUKVI0qBgQkc+SxgWUYaIAqOpKwKXJ6bgGlaaDV/YvHaFNrtDsKTfVSrJeqIg/bRNwjclFIALeP3saybhu8SC4VBHwnhBXXIKocYRXD9QzBi4Xgchmkd9+L+CTAAMqwy+ZzluBgAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-rb { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAixJREFUeNqEUktvElEU/mag5f2yJhXLwxIt0kiqsVEXujP+A925cu1Pce3WtXVtYuJCF7KtTY0NrVQIpRVKeXTkMcO9F8+9ZVooJJ5kcmbmfOe733fO1YbDIWS8+/g1dycVX7W/xyO3vdsuVKqvnE7HZ230783rlyo7bVBicSGyfjsVwozomVbIPe/c+FmsPHfoRKJd1HT7hXHBZjVbA4aA14NnD9bC2VR8gwuxPi5Sx39Cp+M0XUP0ahhP1jLhW7HFD4zze3b93ILtXYyyVKlR8/5hFbnvO9gtlrGSjOF+OpXkYviWyo8mCS4R6bqO4p86vm3v4fC4DrPfw4unj1XN6JvBaQtjChzUXK43sVU4wNFJA43Tv/B73edQwTmfIhAjCVL6UdPAj1IVFSKhCdAcAI9rnjBiAjtBYEu3GEeh1sKJ0YXR68sVIujzIhzwY8DEBHZqiLRKkicQDfvABxaiQTc4Y/C65pCOXwcjcmlvJgHtlwi4epYifiQWgmoLZwPW6HQG07LgcOgKO0UglAKOTt/E+09fwAiUWU7QAE9xUK3jbvomsispZVHMVEDSZdHo9rCZ/4VIMKAu0XGjpU7d2S8hk0pCELHEzrjKnCQOYJoD+Dxu1RyiwUm5LaMDo9NFt2cqDLvY4oQFp/QpfT/MrmI5FkWebt+NpWto0j2QmQkOjZ9hpwhqjXZzM/+7LU+cc7lRrjXh8/lVLRK5ovLWXglOsiOxdt8/AQYAzv8qbmu6vgEAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-rtf { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe5JREFUeNqEU01PE0EYfnZmd5FSvgLYFuwWt9EgHyEaox68eDJevHvwJ/hTPHv1N/QgZ2NC4g3kUAQKFKGhjVKqRrvbnRlnht262FHfy+y8877PPM8z71pCCKh4/ebt+rJfXEz26Vjf2mnsN5rPKKWbVpx7+eK5Xu2kyMtNTd5d8MdhiJ9BOO7atFI9ajy1UyAqSPIRMR6ZmoNehNHMMB7fX/UWvEKFMbYKE8DfQnAhwRmmJkbx6M6S5+WmK2Evup2c9yUk2nnKA0XVcSiGXAe1k5beP1i+4RFCXqnPywB/AKVzK34RjHNYlgVKCH50w7EBBogbTa/AVM5SgBdn0gc2AMDjPsbFPz2xye9asweS6n+NTbG8BCCfUtLjff2WoVnVpAH6z6hMUtJE3EykYfpF4vUiL3QNS7FMeSAQRBHW3r1Hq91B+VoBQRji4+ExFsvz6Hz7jm7Yw5OH92AcJKW9G4SoHhzhy/lXbB98Qmm2oCXN5WawsV2TACEoJXqwTKOsb3BtR2ucmZxANpPB8JUhyPnHWDaDpfJ1eZFALzJJ4MKO5MEtv4TSXB7V/br8iQLMz+almRZWbvoo5q9qRlxwewCgeXbe3qrVO5ZkUD/9jJGRLPaOm6COi92TU1DbxYe9umRD0DrrtJO+XwIMABWp9nS+FgaoAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-sass { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MDNDMTBBM0JGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MDNDMTBBM0NGMTE5MTFFMTg3N0NFOTIyMTQ2QzhBNkQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowM0MxMEEzOUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowM0MxMEEzQUYxMTkxMUUxODc3Q0U5MjIxNDZDOEE2RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Po72XUcAAAJcSURBVHjahFJdTxNBFD1bykc/ttvdtttWGgI0bYrUgDZoNYqRJ014kMRXHvwB/hQTH/wFhMREJfFBQxBjhMRIFEQSCAlQxKYGggiU3e3HbnfX2bFt1EU9k9m9mblz5p4zlzFNExYmpue/jmTSZw5PZAl1MAwDT0c7O72wvPdudeNakPNtOZ0tsM7cvzdOc5yN5LDAsTFRAJks/kC2PxFRVe39Si6f4byez62EpAEH/gNN18F53Ri/Ocxf7OtdLMpKT42s/ZPg1cISJp/P0tg0TBzLCoK8D7eHh4RkLLJ4cCz12AjMXwgez8yhqtVo3NbqRKlcxcSL16gZwJ2Ry8KVc8kZO0HdTKlURn+8G6PD2SZhLMQj96WAiMAh2RXFYKI78lcJcx9WYBCycICnpNbojUWpD5Y0C4Zh2D0w6hWc70uQZC+IWfQZrXF0IsHvY+meBd08haAhoVMMQFJKWF7PNZM+klhRyogGhbqxOIXAMOtEwGAqDqVcgbVkkE+5UsEAWavf0az2t0ZqvK2qabh6IU3joizDwTgwej1LdVfJXkdbK8mt2QkayO99A0/0trQ46I1lVcX+UREhnsP34yLp1AD1xibBMuntpzU8mJyi3Tc1O4+l9U06n7x8Q/8PHz1DrrALt8tlr0CrkbJMHTop9Sk5sLa1g8L+ARJdnShKClY3tunN69t5iGLYTlCtakjFY7gxNABdN3B37BaqqoYT8pyX0in4ORbRkIA46YlDRbUTbBZ2Jb/Pw4qiKFnapcpPo9pdbrg8DjAOBsFgELJmsGs7eWkkc5bu/xBgAHkWC6UPADTOAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-scss { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RkM4QjYyNDVGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RkM4QjYyNDZGMTE4MTFFMTlBREZCNDNEM0ExMTk0MUIiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDpGQzhCNjI0M0YxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGQzhCNjI0NEYxMTgxMUUxOUFERkI0M0QzQTExOTQxQiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pkf1yeMAAAJbSURBVHjahFNdTxNBFD0tLULpB91uodVWPmorUIxo0VSiNSExMYYHE33l0Ud/in+C+OSjYgjRGDBRCKJIUkIEWi0WKlja0ul22+5219lJ26gLeiezuXvn7rnnnrlrUFUVms3Mvd2bjIyezRVLBA0zGAzo6jhjm1te+7EU37rFO+w7JlMbtG+ePJ5mOaZmci/nsPl6ONBtw18WDQc9tZq0sp7YjTisXV/NFKRpRvzHpHodDqsF03djzuvDg6vHJWFAprF/Arxe/oins6+YryoqCiUBvNOO+7FrXMjnWc0WyIAOQP0N4Nn8IqqSzPx2swllsYqZl28gK8DDyRvcxKXQvB6gISYpiwgH+jEVi7YAfW4nEqk0PJwDofNejAX7Pae2sPhhHQoF63U5Gai2Bn1epoPWmmaKoug1UBoMrgwHabIVVCx2jdrKFwm67TZ2plldPQGg2cK5HheIUMbaZqKV9In6giDCy3MNYXECgKI2gICxoQAEsQItpNCHWKngMo01arTY/jFIzbutShJuXh1Fm9FImYiM7tTtKOtbO+toN9Nc+fQ5SGUOIVYl7HzPIH2YRZ0y2KZ+sVzBHn2v1mpMGx0DTaR3nzfwfGEJdybGkdo/wEigDyvxLzg4yiESvojZhfd49OAeLJ2degaSLIPOO6vwgiYaaRErTRREEdn8MeJbSVZ5M7nLdNExqFLaQwEfFfACQn1+HBWKSKb3MT4Sgstuh9vVDa+bQ4DORE6o6RlspzMk9TOPfr+fiLJCLFYr3TZSKNcI7+aJwWQmPM+TkqRg49tu65f/JcAAMwMas6WUKd8AAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-sql { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAh5JREFUeNp8kctrE1EUxr+ZyXMkoa1NBROaSkpTBE23PhZ25cql2y5duvAPUdGFS1FxIRRBXZlFQ9GVdDENIhGJxkDsw2mneZnM83ruNZlOmNoDhzlzz3d/9zv3Sowx8Ch/qlYK2XM3cEJsbH0+qjV/rd6/u6aN18b7RMFT+9aosP/Ex+0ae/puw7j36PlKEMAzctKJ3aGFamMHjV0d+wcGitkMrpWWp6hVIciEk2MAOwbUWjosx0UiFoWqJpGMx5DNzODq5aIPoa82AWBg/lyKLMH1PMp/a9XvLXLzG1cuFlBaWpiKxaIPSLY6CaC93ggQjyiQZRkeQSzLRovGaPciWLt5faSWEBoh6KBvOhiaNga0+Y9pwaFxvu7rfp8F5pWDt+qNMp2IijHGwddWCvN+33/CoAOP5nVdT9SdoQ1JkggiQ6Yvr7V60+9z7akA2gfH9cRF8hO5F5Ve4lQAF9uuK+qFsylkzsQxrcaQm04hdWkR83Mzfp9rQ3fAFzu9Ph6+WMfjl6/pGBdb2jbKmx8QlRjWy5vkyhUZBPgOeGNHN9AbDLGUz6He2hVj3Ll9C8/evsdgaMK0HV8bcmDTU0UUBYXcedR+NLGnH0I3jvDk1Rsy46FP4C/1BtrdntCGHNiOAzWZgEKQ5Qt5lIqLojbaXSQTcRy2OwT4SZqk0IYAOgkVWUE+lxX/zb0DpFNpkTzmZmfFtzewhHYcfwUYAMZmVaZQlLFHAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-tga { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnxJREFUeNp0U89PE0EU/ra725K22ILRGipb22pMG6JcSEQTbUIwnozxpBcvepeEP0KPogcT/wlNT17kIKbEmChFUYKGVtL0R2gLtNCl3Z1Z3+zSAlonmezOe/O+973vvZEsy4JYnqdPMu6RkSQYQ29JEkB+PZcrslrtPhQl23VZc8/tr9I1yMHg0EA8HrBM04lVFAhoY38fSSDQVN3pfKV8G7KcxZHl6v1xblqU3eLc3p2VFZjr6+gQgwsnhzGTuq6Nhs6kYZqXjwL0GFhEl3U60OfnwWs1GGtrUKNRsKkpeIIBpKIRtI1J7cX7hXRhc/MOhXw5DkCZGG2zXAajzFIoBMvng1ypIKOqmP30GW3OIEcimovzlxRy5RgAFwDEAIODkCcmIMdiQLsNdWwMZdJlg8pzEUt1aBhKq3XinxKYqF9yQbqRIqsMy+0Gyy47bKgUWXSLtDENE5wdtuqQATm50F1VnPbRGeEw8HXZbiV8fsDvI9ldju9vADAyihLEbrWAZhOoVp3z6iqBUiB1A4nEfwCEsbkL/M4TgE5n5jDx+oTEzp1d8m9tC8H6MaAB0imzx0NU/WKUYE+loEyawDBo2ui6TGfT6ANAxrvx87gYCGCxXEKVJvCWFsG3eh1vN/J4OD6Od4UC8o0G3TX7TGLHwI9iEQmvF9X6Fh7F4/iYy+GcLOMSlfEgGsP0qdNOmX0BiGKpVkV1bw/1nW2b/gCpf1PTcI+Y7eg6ps+G4bG4PR99SjAVo9HE4q+fKNE0vl5awuSohjeijbRefVjAtUgEQRK7Yhi9OKn7nKWZxxlSPWl3QwgnaIrW8QMhD542vUbx/W49m7sq4v4IMABOqi3Ej7bAEAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-tgz { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAnhJREFUeNpsU1trE0EYPbMzSTfdtInFtkkpiaXVWou2FRUEn/so6JugL/oH/Af+B1988if40jcFERQURNBSQdDWlLQN2lsue8neZsZvc7FoOrDszM75znfOmVmmtUYyvry++36yfOeS1qqzDtvH2P76ApPlW3Drb2sHex/uccHWAdbZX30kO2+B3siN3zhTnHuQ66+95i423jzFzOVljBdKOZNHazvVT7e5wF+SZBj9iZJ+3J11mbW2kR8T4LwFli5i4fqTUvnczTUp9RLtDhKgJx0q4dEwWAxrREKICHEsoYYXMXvlcWmquLgmY71yCkG/c0AkARgLMZpnMDMpGNzEYe0dGp6HwvmHpbHC1Wf9MnFCkHQOyYEPzSJwQ2B65Tm5NZG3Fshim6wbMNJn4bpHowMKtIqo2COgR2IcAptwjvcgo6i77igjEmVDqbY8xQJ1VwRULhiBI6+G9Zf3cbTziuzIDkmHSNqECTFgQScEcYuc2NA8TcdYwXD+GkK/TYVN+u72WrIudiAD8o6oAR2RRCmQMjis3CIy1iSpPySCXhFTXeyAgh4BR+JVw8pauLi0Cp4yCX9A90FQhnSBYtnF/k+Q+HYam9itfIZB3QvT8zj8XSW5EhNTs9ivbSLwPUzPLNPJBIMEKnaQYg6aB9+RGR5F5VsNgnNKXMI1NdJGG5WfHzFVLJ7k8c8xUngpVodlDSGbFYj8Y4yMpOG09lHf3yIFPzA3fwHZTAQVtU4JUTeFDrdgDdlI8wAz5Qy2KxswReI7QODZcOr0ZH3q2hIDBI7zq16tuk3FNPxAI4wN+pkoccYoE4YJU5EdUtM4Qst26v26PwIMAKj3P/2YUKgYAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-tiff { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmRJREFUeNp0UktPE1EU/qYzHWstlrYJNcWUElyUJsaNGh9B0g1Lo0v9Ey78EbrVxBhXuHShm25YGBJRQpAYBDEWpaEPEhksdVpbyjzveO4MfZDCTWbauefc736PIziOA77OPH2yJCcSGdg2uksQAKofFou/7VrtASRpvVNynj13f6XOhjg8HAlMTIQdy/LO+v3uYUPTkAHCTb+cK+0pdyGK6+hbvu4/xiyHbncYAwfR19ZgbG/DoO9LsSgeTd9JXoxfyMG2rvQDdBlwIZauQ5ufh12twioU4E+nYU1NIRCNIDs+Bt28mXzx8VNuZ796j9q/DgAwomwqClilAmF0FE4wCInAlkjO4y+r0JgNX2os6XPYS2q/cQyAcQatFjA0BPH6NYipccAwIGUy2CVJFZInkKlyJAqx3T4/IMGmJkeWIWSz5KgI5pdhb3yDXS5DSCYh8rTID8s0wexeVD0GtMd85KkkefFxUfE47M1NokbJkByEQl6tL+ouAI+MUwbFhnYbaJKc/Sqg0x4H4eDRGDA56fUOABA9/GsCpaIHwr8FOhQ823O5RfW66tUGADhNy3RNRDjcN41HLxdQ8J6jYTsOQLfOJBK4f+s2/uoathoNGKT1MtFeVHZxdWTEZfEq/wMKl3rCJOIzTV6ADs2R5ulYDDNkYjp0DhrF+zCVgkw31+v1UxjQZkNV0SADd2o1MIuc9gmY+/kLxb0/UFoHePd9A1qzeUoKpilx9xcLWzgg+u/zeVfuQqkM9bCN1ysrWKXxdtPgvScwUAm58XZ52W16QyPtifRUzi588GbEi1ztHPsvwAC4uC9qhnsZvwAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-txt { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAeJJREFUeNp8UrtOG1EQPfsyXiyzBguIJSyChZBBEFCKpKHLo6egpErNn8CHgH8gkZIiTSIXLhJAWCgkoMgRMSiRBSK29z4y9+I1d/HCrFb3MTPnnjkzlpQSynY+fP70fGF2gQuByCz6lfdd9Uurfvrrjes6762eb3tzQ69uFJwPsqOPC+MBEmxxphi4tlU5OGmsOzaBWLc+O9oIIVhScidkyGZ8vH62nHtSKlaI4cse6TjAfSaFBBcco0EWqyvzubmpyQrj/FXk75cQaSEMeMXU8xykPA/Hjd/6/LRcyjEpt2i7HAe4A2TeLZWKUOJaVLxj27j813EHGKCXaAJExu/4BOdiAED08riQD2riOrexyRoYc3CvsAbLGAAjZga7vgZG23WMCdBvoxKJc36TRBlMiaa2JByjNqqD8qkYc1pjDK7abey+/YhrWlfKswhpiCR96aEU9o5+QE3g2ovVWDm2Sc22bBQm8vrVpbkS9r+doPr1EOWZaQ0yFoxg2PcREosEAI4uvZhJpzFMP+cSXRbq+043RManez+tNWKMI6GN0g0Z04HFR+NoNC/0yx717efZOSbzY3AcR4Op2AGA5p/W31r9e0vNgSrh9OwCrpeCkqvZuqTybnpRqx/r2CjvvwADAJC/7lzAzQmwAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-wav { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAApFJREFUeNpsU1tPE0EYPXtpKbX0wqUQKVQMFdIXQBNCQBs06KP+B8ODGh+Mf4b/4IsGE54kxhcMBrkp7YOQgBRvSKG73fvsrt8Otoask0xmd+b7zpxzvm8E3/cRjPkniyulW0NFy2JoDkEAguOlpXJ9p3L8MBqVl4O9YHxae8pXuRlcGO7KPLhfTDVUqwUgigJMy4Whm6lEXHjxYf3XnByRN0QB/2KaH7btMlUxoRJAcyqKhdOaht7+DJ49n+2cvTnwynXcsb+kLwJ4rgfmMDDGWqvneXCZS9ND7mov5h9ND85M9y86Dpto5rUkuJ4Py3YDJpy6QGJPayqB+Njf+43XL220t0cwOZkfrNXsBUqZugDA6CbLdAiAwaek1ZU9LmP8Rh6S78GsGxjOp9FdzKJaVZIhBgGASzK21w/wbrnCk8euX+EMAjaaZuPHdwUdHVFYluuGPGCORwwYjg5rqOwccRk+3Ux0IEvntmsNG4ZmUayL/wAwKHUNfZfTKN0ZRaw9Cof8qJ/pMAyHy5KkAMTksSEJtnMenM7EMVMawbejMzJRh67bXEYiIXEAVTW50SEAhzqwfqrBcXx4VOhYm4RsNgHbsJFOyZTsQ1MN+hcohoUlkFiMT+TQFpMwXOjGpXgE+XwGk1N5pFJtKNCequgYGupCRBbCDOp0KBJc4VoP3dyBONW8uydBgBHUThqQKCk3mEZ/LoUG+RBioJO7VarAwEAntjYPiUUW9Hh4b2R7k9j98hN37xWx8fGAt3eIAdVMLn+uUv+b2KReSCZjZJiB9bV9jIz2ofr1BKvvd7G9dRC80lae0HzOt+cWVnrSKDrMJykifwNBpCgE/UAllEXufmDu8Zlffvvm8XSQ90eAAQA0pF7c08o4PAAAAABJRU5ErkJggg==); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-wmv { - background-image:url("data:image/svg+xml;charset=utf8,%3Csvg id='Layer_2' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 72 100'%3E%3Cstyle/%3E%3ClinearGradient id='SVGID_1_' gradientUnits='userSpaceOnUse' x1='36.2' y1='101' x2='36.2' y2='3.005' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23e2cde4'/%3E%3Cstop offset='.17' stop-color='%23e0cae2'/%3E%3Cstop offset='.313' stop-color='%23dbc0dd'/%3E%3Cstop offset='.447' stop-color='%23d2b1d4'/%3E%3Cstop offset='.575' stop-color='%23c79dc7'/%3E%3Cstop offset='.698' stop-color='%23ba84b9'/%3E%3Cstop offset='.819' stop-color='%23ab68a9'/%3E%3Cstop offset='.934' stop-color='%239c4598'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill='url(%23SVGID_1_)'/%3E%3Cpath d='M45.2 1l27 26.7V99H.2V1h45z' fill-opacity='0' stroke='%23882383' stroke-width='2'/%3E%3Cpath d='M9.1 91.1L4.7 72.5h3.9l2.8 12.8 3.4-12.8h4.5l3.3 13 2.9-13h3.8l-4.6 18.6h-4L17 77.2l-3.7 13.9H9.1zm22.1 0V72.5h5.7l3.4 12.7 3.4-12.7h5.7v18.6h-3.5V76.4l-3.7 14.7h-3.7l-3.7-14.7v14.7h-3.6zm26.7 0l-6.7-18.6h4.1l4.8 13.8 4.6-13.8h4L62 91.1h-4.1z' fill='%23fff'/%3E%3ClinearGradient id='SVGID_2_' gradientUnits='userSpaceOnUse' x1='18.2' y1='50.023' x2='18.2' y2='50.023' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3ClinearGradient id='SVGID_3_' gradientUnits='userSpaceOnUse' x1='11.511' y1='51.716' x2='65.211' y2='51.716' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='.005' stop-color='%23963491'/%3E%3Cstop offset='1' stop-color='%2370136b'/%3E%3C/linearGradient%3E%3Cpath d='M64.3 55.5c-1.7-.2-3.4-.3-5.1-.3-7.3-.1-13.3 1.6-18.8 3.7S29.6 63.6 23.3 64c-3.4.2-7.3-.6-8.5-2.4-.8-1.3-.8-3.5-1-5.7-.6-5.7-1.6-11.7-2.4-17.3.8-.9 2.1-1.3 3.4-1.7.4 1.1.2 2.7.6 3.8 7.1.7 13.6-.4 20-1.5 6.3-1.1 12.4-2.2 19.4-2.6 3.4-.2 6.9-.2 10.3 0m-9.9 15.3c.5-.2 1.1-.3 1.9-.2.2-3.7.3-7.3.3-11.2-6.2.2-11.9.9-17 2.2.2 4 .4 7.8.3 12 4-1.1 7.7-2.5 12.6-2.7m2-12.1h1.1c.4-.4.2-1.2.2-1.9-1.5-.6-1.8 1-1.3 1.9zm3.9-.2h1.5V38h-1.3c0 .7-.4.9-.2 1.7zm4 0c.5-.1.8 0 1.1.2.4-.3.2-1.2.2-1.9h-1.3v1.7zm-11.5.3h.9c.4-.3.2-1.2.2-1.9-1.4-.4-1.6 1.2-1.1 1.9zm-4 .4c.7.2.8-.3 1.5-.2v-1.7c-1.5-.4-1.7.6-1.5 1.9zm-3.6-1.1c0 .6-.1 1.4.2 1.7.5.1.5-.4 1.1-.2-.2-.6.5-2-.4-1.9-.1.4-.8.1-.9.4zm-31.5.8c.4-.1 1.1.6 1.3 0-.5 0-.1-.8-.2-1.1-.7.2-1.3.3-1.1 1.1zm28.3-.4c-.3.3.2 1.1 0 1.9.6.2.6-.3 1.1-.2-.2-.6.5-2-.4-1.9-.1.3-.4.2-.7.2zm-3.5 2.8c.5-.1.9-.2 1.3-.4.2-.8-.4-.9-.2-1.7h-.9c-.3.3-.1 1.3-.2 2.1zm26.9-1.8c-2.1-.1-3.3-.2-5.5-.2-.5 3.4 0 7.8-.5 11.2 2.4 0 3.6.1 5.8.3M33.4 41.6c.5.2.1 1.2.2 1.7.5-.1 1.1-.2 1.5-.4.6-1.9-.9-2.4-1.7-1.3zm-4.7.6v1.9c.9.2 1.2-.2 1.9-.2-.1-.7.2-1.7-.2-2.1-.5.2-1.3.1-1.7.4zm-5.3.6c.3.5 0 1.6.4 2.1.7.1.8-.4 1.5-.2-.1-.7-.3-1.2-.2-2.1-.8-.2-.9.3-1.7.2zm-7.5 2H17c.2-.9-.4-1.2-.2-2.1-.4.1-1.2-.3-1.3.2.6.2-.1 1.7.4 1.9zm3.4 1c.1 4.1.9 9.3 1.4 13.7 8 .1 13.1-2.7 19.2-4.5-.5-3.9.1-8.7-.7-12.2-6.2 1.6-12.1 3.2-19.9 3zm.5-.8h1.1c.4-.5-.2-1.2 0-2.1h-1.5c.1.7.1 1.6.4 2.1zm-5.4 7.8c.2 0 .3.2.4.4-.4-.7-.7.5-.2.6.1-.2 0-.4.2-.4.3.5-.8.7-.2.8.7-.5 1.3-1.2 2.4-1.5-.1 1.5.4 2.4.4 3.8-.7.5-1.7.7-1.9 1.7 1.2.7 2.5 1.2 4.2 1.3-.7-4.9-1.1-8.8-1.6-13.7-2.2.3-4-.8-5.1-.9.9.8.6 2.5.8 3.6 0-.2 0-.4.2-.4-.1.7.1 1.7-.2 2.1.7.3.5-.2.4.9m44.6 3.2h1.1c.3-.3.2-1.1.2-1.7h-1.3v1.7zm-4-1.4v1.3c.4.4.7-.2 1.5 0v-1.5c-.6 0-1.2 0-1.5.2zm7.6 1.4h1.3v-1.5h-1.3c.1.5 0 1 0 1.5zm-11-1v1.3h1.1c.3-.3.4-1.7-.2-1.7-.1.4-.8.1-.9.4zm-3.6.4c.1.6-.3 1.7.4 1.7 0-.3.5-.2.9-.2-.2-.5.4-1.8-.4-1.7-.1.3-.6.2-.9.2zm-3.4 1v1.5c.7.2.6-.4 1.3-.2-.2-.5.4-1.8-.4-1.7-.1.3-.8.2-.9.4zM15 57c.7-.5 1.3-1.7.2-2.3-.7.4-.8 1.6-.2 2.3zm26.1-1.3c-.1.7.4.8.2 1.5.9 0 1.2-.6 1.1-1.7-.4-.5-.8.1-1.3.2zm-3 2.7c1 0 1.2-.8 1.1-1.9h-.9c-.3.4-.1 1.3-.2 1.9zm-3.6-.4v1.7c.6-.1 1.3-.2 1.5-.8-.6 0 .3-1.6-.6-1.3 0 .4-.7.1-.9.4zM16 60.8c-.4-.7-.2-2-1.3-1.9.2.7.2 2.7 1.3 1.9zm13.8-.9c.5 0 .1.9.2 1.3.8.1 1.2-.2 1.7-.4v-1.7c-.9-.1-1.6.1-1.9.8zm-4.7.6c0 .8-.1 1.7.4 1.9 0-.5.8-.1 1.1-.2.3-.3-.2-1.1 0-1.9-.7-.2-1 .1-1.5.2zM19 62.3v-1.7c-.5 0-.6-.4-1.3-.2-.1 1.1 0 2.1 1.3 1.9zm2.5.2h1.3c.2-.9-.3-1.1-.2-1.9h-1.3c-.1.9.2 1.2.2 1.9z' fill='url(%23SVGID_3_)'/%3E%3ClinearGradient id='SVGID_4_' gradientUnits='userSpaceOnUse' x1='45.269' y1='74.206' x2='58.769' y2='87.706' gradientTransform='matrix(1 0 0 -1 0 102)'%3E%3Cstop offset='0' stop-color='%23f9eff6'/%3E%3Cstop offset='.378' stop-color='%23f8edf5'/%3E%3Cstop offset='.515' stop-color='%23f3e6f1'/%3E%3Cstop offset='.612' stop-color='%23ecdbeb'/%3E%3Cstop offset='.69' stop-color='%23e3cce2'/%3E%3Cstop offset='.757' stop-color='%23d7b8d7'/%3E%3Cstop offset='.817' stop-color='%23caa1c9'/%3E%3Cstop offset='.871' stop-color='%23bc88bb'/%3E%3Cstop offset='.921' stop-color='%23ae6cab'/%3E%3Cstop offset='.965' stop-color='%239f4d9b'/%3E%3Cstop offset='1' stop-color='%23932a8e'/%3E%3C/linearGradient%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill='url(%23SVGID_4_)'/%3E%3Cpath d='M45.2 1l27 26.7h-27V1z' fill-opacity='0' stroke='%23882383' stroke-width='2' stroke-linejoin='bevel'/%3E%3C/svg%3E"); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-xls { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmxJREFUeNpsU0trFEEQ/mamZ3Y2+0zIC2MmITEkUYgERFQErx5E8KTi1b/h79A/4SW3nCNeYggBYZVEMU/y3N3Z7M7OTD/G6lk2ruw20zRdU/XV91VVG0mSQK/3n1a/jky6d6Xs3G8WXS+Pw5N6LXjLLGuna/78oZKerGsYKtrDE16uJGL1L9gEOOcYd2dL1fNwrbL//aXN7J1efPMmkUqEFAk0A0VZNbFEaQCBscIkXj975y3NLq9xye8PBkAniHOFph+j2eC4rsdoB4LsFubGl/Hq8RtvYWpxTQi52o1jvWiGYaRZL0/auDgOkC/Z8BYL2Pqxidp1FZkhoDxpeaXA/Ujuj/4HoOxKKjiOiek7RUShRNQWaNYFQuMafrYCxiw4ozZKfqbYJ0EvRdl1DQyyTs8XCNTA6UELMwvDyLpZWIZNNlNLlQOK2LMJRJ+5AkuZ1S7CFFzJzk56GnUjQWlYkqCoBWFbonEVYcLLA4dNnB624GQsDBWIgfZJEgxkoChzSFWvn4VpQemDm2VwXQsXJwF1h6c+gxlQ5jgSiEUEt0wdIe7tMES+nEG2aCLiJMOIIWIr9e0DEELAMUrwRuchVAyTKimUwO75Jm6VF3Bv7imOaj+xd7UFKVS/BPJF1b/E4tgTrE49J60O5kceoNqowiuuYKa8ghHXA48U9MT2AQgyRvTThE30bQiaSGa4yLMJNFo+Dq/2cHt4CYlwyFf2S6BHwwrMw/avDbR5C1k7h1YQ4KH3Amf+AcZyEbZPv9CItzQD1l9EbtYOjv74v/d3O9RMPTDrsEwGIWN8q2yk7XNYRs9JrRv3V4ABADSGR6eQ0/NQAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-xlsx { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAmlJREFUeNpsU8tqFEEUPVXdPY/ueWZIoiYZiSYKYhJc6EbduHOhgijo3t/wH1z6B0JAhOyMILhxo4kJGk1ASTAxwWF0Mpp5dHc9vFUzYwidaoqmq+8959xzbzGtNcx69PTS26ETmQtS9r4Hy/xv7MW7jV+th5yzVcaYPX/++It9u4NAv+CVR6tBUUTqMJsDcRzjZOZM8W9ZLKx+/XDb4e5/kH5In0lpIYWGUaC0YTZnBCAEKoVR3L36oDo7NbsglZwbqD6iQKOXFMcKUVfBkBAoQhlD5xxMDp/HrSv3q1JgYW3z0x0KXzkCYJaRZljru23aHWTzLiamAyytv0O9UYdf5PArqlppBfMUfu4oALErqZBKcUxMFRCHEp0DgW5Lo4N9NIN1dF0XXsVFOUyPJTzo+WBANDidjp8tgHGG3c0DnJ4uIRf4cOCBaW5KjY8xkZL72xpJ9QcFz5bVqHUJGHZL2YtNmKi06YCyiVFb4s/vEKMTAf1p4edOG6mMi1zR6wEpdUwX+vLDtkCzHoK7ptcM6ayLmGajvtex4PliyoIkFRjmUEASelB2rXQRSfjUCT9PlWpmW21iTGzCAyEkUixPRqXhe2V4zKczbdmybgkpJ0cGOuA6Y2MTCsKoi5HsNK7N3MN+uwYaWbxYfoLLkzdxcew6lrYWaZhm8PHHG3zffp1UwJSHz9vvkU8PodbcQYYYS5lxYkxTkGdVDQdV1Js1qPgYD6JIuIE7gsXVefIhIuM05k7dwMbeMmh87a18ufIMaVYyprrJLgje2Nr+1tzYXANnDnr3zRhHj37Vvy2wpXHtNAd5/wQYAD6WMuT2CwoVAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-xml { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAilJREFUeNqMks1PE0EYxh+g3W2t1G0sEqyISynUFJsSOShNwCamiYZED3LgIkcuxoN/iCZePZiYGD2aGD+i0F5KMChxlVaakAK2ykcAt+WzdLu7zkxo3WZL4pu8mXfmeeY3885ug67roPFh5nvc62m9hjoR+5LMp7MrkYf370qVtco+VtCUFpbj+jGR+JbWn76OyQ8ePwsZATQb8R/hanZgINgj9IqeuBFCw1Kt9OMBnNWCs24XwkG/QKYUEiGjVAPQof/rq0783pShET3ULQo8xz0iS5FaANmrHQH2DoqY+DSLSz6RzecWlnD9ymU47LYjd4O5BXqDTG4FM3NpTEkpdJ5rw0AowLRMbhUfp58gTOaD/UHmNQPI6YmvKWRX1zESHUJ/oBs2nmPa+Mgw0ZIM3tZyGoJwygzQNB2jNyJIZX7iB0lpPoM70UGmPX8zCU+rG8NDVxHwdiC5mKsPUFUN/gvtLLf39sFzVqaN3YrC6TjBauqhXhNA1TQoqloV7Da+pjZq1FsXUCamF29j6LvYhf3iISamZ3Fv9DZevouhRzzPfOG+3hpA9U9UyioOlTJ7pFeTCQS6RGzIebyf+oz5pSzWtmSW1EO9phvQ00slBRt/8qR3DoWdXbiczUiTzd52D+tdLmyTB14mx1rMAKVcRpEATjrsuElee/HXGmnFRyBOGD30C/nEDjNgs7CDpsYmnHG3YPegBCvHs9oYfm8nG9dJa5X4K8AAQzQX4KSN3wcAAAAASUVORK5CYII=); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-yml { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAdxJREFUeNqMUl1rE0EUPbM7m5Y0Zptu21AwWwhYpfSDFh+kvvRd8N0Hf4I/xWdf/Q158F0QoQ+CVsFKaLSQpt/dpmvztTOzzky6cetOpWcZZvbO3MO5514SxzEU3r57/3GpWllM/tP4sL3TarROXuSo/SWJvX71Uu80Cfhlr/T4UdWFAVfdnmsTUtvdP35OUyQKVnJgXDBTcj9icAsTeLax7j/052qM81UjwW1QJXEhMF0qYnN90fdnvdogYmvJPU0/VBApD4hcDrWRcyikfB17srzgW7b9Rh1vEvxDlI4tVytaBSEEtmWh0xsUMwpwnWjqAlcxogiHd1wiQyCu87iI/+sJtf6+NXsgpd7FWCMB50KvkYMGMbLdZgLlfj+K9K4+FnFQ2x7WntIs50AbmiGwLILt+k+EvzvSNIHzdigdJ/AmXQRhiHv5POSwYmG+cqPVo0HqDxj8uTK2vn1Hfa+JmdIkvtZ/4fOPXU3WPDpFeNWVyUKryCiIGMN4zsH98gym3CIcOTwT+XHdXrdQQHAZotE8kBPpSqPNHtBOr48HUmLOcXRJT9dWNMGYJFby91pHOAvaykSaITg+bwefdhrteDRTMSwyrFCgI88E056Hy+4Ah2cXQZL3R4ABALUe7fqXWFN6AAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} - -.ipfs-zip { - background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAm9JREFUeNpsk0tv00AUhc+MY6dOmgeFJg1FoVVpUWlFC0s2IFF1jxBbhKj4BSxYdscPYcEmQmIDq0gsERIViy4TpD7VFzF1Ho5je2a4thOqNhlp5Mz4zudzzp0wpRTC8fPrk0/TC6+fDtYicLH97T1Kc2vQDcs+rH3eUAxVznn0fn1DRM8E+iOdv5ct3XmZG6yVlNj6solUbgVTt0q5FGtX6vXqC6VklTE+KAO/OODHSIQPRQpsXC+kkEz2ELA0ystv84tLzyucsbWByisAGf+QAS2CCDRRLMJMmxC+i8C4jdLCm/zM7OOKFGptcO6/BTpJ0yeQB0Y+mfKQuZZG0jQgeRbW8Xdomobs9LN8scc+UPHNy4Dwq8IljotIIQEm59/RoSyM1CKkXKZNBm7kIVgyM6wgAnSgRK9vqQfHPiMFDHqyFVsLR9Cm0o4YzoAASrSjCelQfRPb1Vc4qn0EY5L2W9GEaBLcxQgFHpGbkMIDJ69e+wjJ8VXqRgKid0r7ftQdxkRs9SqA2kgAm14SSIQh9uhuLGPMnKJs/5KquL1x0N0RCsizigoDaLqBdHoMiyvrlBsHVx1wphD4BCewoqxGKKDwAgtOy8JufYuk+5golGGaGZwc1sIGoDz3AOPZSVLaHgVwydoJDM1H4DbQODughB3YpOD44HfoHgnu4e7So0uAi0stHLJ3Aud8B9bpHu6vPoSu9TtDl6tUuoFiIYOgu0+158MKmOxomtyD3Qi/3MTR7i8K0EDG1GHO5DE3X4DvNahZlJOwEkOATvdPc2//hx3mXJ5lFJaF8K8bStd0YGfnOJbMGex21x6c+yfAAOlIPDJzr7cLAAAAAElFTkSuQmCC); - background-repeat:no-repeat; - background-size:contain -} diff --git a/extern/go-libipfs/gateway/assets/src/style.css b/extern/go-libipfs/gateway/assets/src/style.css deleted file mode 100644 index 3e7b8a734..000000000 --- a/extern/go-libipfs/gateway/assets/src/style.css +++ /dev/null @@ -1,212 +0,0 @@ -body { - color:#34373f; - font-family:"Helvetica Neue", Helvetica, Arial, sans-serif; - font-size:14px; - line-height:1.43; - margin:0; - word-break:break-all; - -webkit-text-size-adjust:100%; - -ms-text-size-adjust:100%; - -webkit-tap-highlight-color:transparent -} - -a { - color:#117eb3; - text-decoration:none -} - -a:hover { - color:#00b0e9; - text-decoration:underline -} - -a:active, -a:visited { - color:#00b0e9 -} - -strong { - font-weight:700 -} - -table { - border-collapse:collapse; - border-spacing:0; - max-width:100%; - width:100% -} - -table:last-child { - border-bottom-left-radius:3px; - border-bottom-right-radius:3px -} - -tr:first-child td { - border-top:0 -} - -tr:nth-of-type(even) { - background-color:#f7f8fa -} - -td { - border-top:1px solid #d9dbe2; - padding:.65em; - vertical-align:top -} - -#page-header { - align-items:center; - background:#0b3a53; - border-bottom:4px solid #69c4cd; - color:#fff; - display:flex; - font-size:1.12em; - font-weight:500; - justify-content:space-between; - padding:0 1em -} - -#page-header a { - color:#69c4cd -} - -#page-header a:active { - color:#9ad4db -} - -#page-header a:hover { - color:#fff -} - -#page-header-logo { - height:2.25em; - margin:.7em .7em .7em 0; - width:7.15em -} - -#page-header-menu { - align-items:center; - display:flex; - margin:.65em 0 -} - -#page-header-menu div { - margin:0 .6em -} - -#page-header-menu div:last-child { - margin:0 0 0 .6em -} - -#page-header-menu svg { - fill:#69c4cd; - height:1.8em; - margin-top:.125em -} - -#page-header-menu svg:hover { - fill:#fff -} - -.menu-item-narrow { - display:none -} - -#content { - border:1px solid #d9dbe2; - border-radius:4px; - margin:1em -} - -#content-header { - background-color:#edf0f4; - border-bottom:1px solid #d9dbe2; - border-top-left-radius:3px; - border-top-right-radius:3px; - padding:.7em 1em -} - -.type-icon, -.type-icon>* { - width:1.15em -} - -.no-linebreak { - white-space:nowrap -} - -.ipfs-hash { - color:#7f8491; - font-family:monospace -} - -@media only screen and (max-width:500px) { - .menu-item-narrow { - display:inline - } - .menu-item-wide { - display:none - } -} - -@media print { - #page-header { - display:none - } - #content-header, - .ipfs-hash, - body { - color:#000 - } - #content-header { - border-bottom:1px solid #000 - } - #content { - border:1px solid #000 - } - a, - a:visited { - color:#000; - text-decoration:underline - } - a[href]:after { - content:" (" attr(href) ")" - } - tr { - page-break-inside:avoid - } - tr:nth-of-type(even) { - background-color:transparent - } - td { - border-top:1px solid #000 - } -} - -@-ms-viewport { - width:device-width -} - -.d-flex { - display:flex -} - -.flex-wrap { - flex-flow:wrap -} - -.flex-shrink-1 { - flex-shrink:1 -} - -.ml-auto { - margin-left:auto -} - -.table-responsive { - display:block; - width:100%; - overflow-x:auto; - -webkit-overflow-scrolling:touch -} diff --git a/extern/go-libipfs/gateway/assets/test/go.mod b/extern/go-libipfs/gateway/assets/test/go.mod deleted file mode 100644 index 8980d9a71..000000000 --- a/extern/go-libipfs/gateway/assets/test/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module gateway-test - -go 1.19 diff --git a/extern/go-libipfs/gateway/assets/test/main.go b/extern/go-libipfs/gateway/assets/test/main.go deleted file mode 100644 index 96d940496..000000000 --- a/extern/go-libipfs/gateway/assets/test/main.go +++ /dev/null @@ -1,156 +0,0 @@ -package main - -import ( - "fmt" - "html/template" - "net/http" - "net/url" - "os" -) - -const ( - directoryTemplateFile = "../directory-index.html" - dagTemplateFile = "../dag-index.html" - - testPath = "/ipfs/QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7/a/b/c" -) - -var directoryTestData = DirectoryTemplateData{ - GatewayURL: "//localhost:3000", - DNSLink: true, - Listing: []DirectoryItem{{ - Size: "25 MiB", - Name: "short-film.mov", - Path: testPath + "/short-film.mov", - Hash: "QmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR", - ShortHash: "QmbW\u2026sMnR", - }, { - Size: "23 KiB", - Name: "250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", - Path: testPath + "/250pxيوسف_الوزاني_صورة_ملتقطة_بواسطة_مرصد_هابل_الفضائي_توضح_سديم_السرطان،_وهو_بقايا_مستعر_أعظم._.jpg", - Hash: "QmUwrKrMTrNv8QjWGKMMH5QV9FMPUtRCoQ6zxTdgxATQW6", - ShortHash: "QmUw\u2026TQW6", - }, { - Size: "1 KiB", - Name: "this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", - Path: testPath + "/this-piece-of-papers-got-47-words-37-sentences-58-words-we-wanna-know.txt", - Hash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - ShortHash: "bafy\u2026bzdi", - }}, - Size: "25 MiB", - Path: testPath, - Breadcrumbs: []Breadcrumb{{ - Name: "ipfs", - }, { - Name: "QmFooBarQXB2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", - Path: testPath + "/../../..", - }, { - Name: "a", - Path: testPath + "/../..", - }, { - Name: "b", - Path: testPath + "/..", - }, { - Name: "c", - Path: testPath, - }}, - BackLink: testPath + "/..", - Hash: "QmFooBazBar2mzChmMeKY47C43LxUdg1NDJ5MWcKMKxDu7", -} - -var dagTestData = DagTemplateData{ - Path: "/ipfs/baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa", - CID: "baguqeerabn4wonmz6icnk7dfckuizcsf4e4igua2ohdboecku225xxmujepa", - CodecName: "dag-json", - CodecHex: "0x129", -} - -func main() { - mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/dag": - dagTemplate, err := template.New("dag-index.html").ParseFiles(dagTemplateFile) - if err != nil { - http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) - return - } - err = dagTemplate.Execute(w, &dagTestData) - if err != nil { - http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) - return - } - case "/directory": - directoryTemplate, err := template.New("directory-index.html").Funcs(template.FuncMap{ - "iconFromExt": func(name string) string { - return "ipfs-_blank" // place-holder - }, - "urlEscape": func(rawUrl string) string { - pathURL := url.URL{Path: rawUrl} - return pathURL.String() - }, - }).ParseFiles(directoryTemplateFile) - if err != nil { - http.Error(w, fmt.Sprintf("failed to parse template file: %s", err), http.StatusInternalServerError) - return - } - err = directoryTemplate.Execute(w, &directoryTestData) - if err != nil { - http.Error(w, fmt.Sprintf("failed to execute template: %s", err), http.StatusInternalServerError) - return - } - case "/": - html := `

Test paths: DAG, Directory.` - _, _ = w.Write([]byte(html)) - default: - http.Redirect(w, r, "/", http.StatusSeeOther) - } - }) - - if _, err := os.Stat(directoryTemplateFile); err != nil { - wd, _ := os.Getwd() - fmt.Printf("could not open template file %q, relative to %q: %s\n", directoryTemplateFile, wd, err) - os.Exit(1) - } - - if _, err := os.Stat(dagTemplateFile); err != nil { - wd, _ := os.Getwd() - fmt.Printf("could not open template file %q, relative to %q: %s\n", dagTemplateFile, wd, err) - os.Exit(1) - } - - fmt.Printf("listening on localhost:3000\n") - _ = http.ListenAndServe("localhost:3000", mux) -} - -// Copied from ../assets.go -type DagTemplateData struct { - Path string - CID string - CodecName string - CodecHex string -} - -type DirectoryTemplateData struct { - GatewayURL string - DNSLink bool - Listing []DirectoryItem - Size string - Path string - Breadcrumbs []Breadcrumb - BackLink string - Hash string -} - -type DirectoryItem struct { - Size string - Name string - Path string - Hash string - ShortHash string -} - -type Breadcrumb struct { - Name string - Path string -} diff --git a/extern/go-libipfs/gateway/gateway.go b/extern/go-libipfs/gateway/gateway.go deleted file mode 100644 index e3f8b080f..000000000 --- a/extern/go-libipfs/gateway/gateway.go +++ /dev/null @@ -1,119 +0,0 @@ -package gateway - -import ( - "context" - "net/http" - "sort" - - "github.com/filecoin-project/boost/extern/go-libipfs/blocks" - "github.com/filecoin-project/boost/extern/go-libipfs/files" - cid "github.com/ipfs/go-cid" - iface "github.com/ipfs/interface-go-ipfs-core" - "github.com/ipfs/interface-go-ipfs-core/path" -) - -// Config is the configuration used when creating a new gateway handler. -type Config struct { - Headers map[string][]string -} - -// API defines the minimal set of API services required for a gateway handler. -type API interface { - // GetUnixFsNode returns a read-only handle to a file tree referenced by a path. - GetUnixFsNode(context.Context, path.Resolved) (files.Node, error) - - // LsUnixFsDir returns the list of links in a directory. - LsUnixFsDir(context.Context, path.Resolved) (<-chan iface.DirEntry, error) - - // GetBlock return a block from a certain CID. - GetBlock(context.Context, cid.Cid) (blocks.Block, error) - - // GetIPNSRecord retrieves the best IPNS record for a given CID (libp2p-key) - // from the routing system. - GetIPNSRecord(context.Context, cid.Cid) ([]byte, error) - - // GetDNSLinkRecord returns the DNSLink TXT record for the provided FQDN. - // Unlike ResolvePath, it does not perform recursive resolution. It only - // checks for the existence of a DNSLink TXT record with path starting with - // /ipfs/ or /ipns/ and returns the path as-is. - GetDNSLinkRecord(context.Context, string) (path.Path, error) - - // IsCached returns whether or not the path exists locally. - IsCached(context.Context, path.Path) bool - - // ResolvePath resolves the path using UnixFS resolver. If the path does not - // exist due to a missing link, it should return an error of type: - // https://pkg.go.dev/github.com/ipfs/go-path@v0.3.0/resolver#ErrNoLink - ResolvePath(context.Context, path.Path) (path.Resolved, error) -} - -// A helper function to clean up a set of headers: -// 1. Canonicalizes. -// 2. Deduplicates. -// 3. Sorts. -func cleanHeaderSet(headers []string) []string { - // Deduplicate and canonicalize. - m := make(map[string]struct{}, len(headers)) - for _, h := range headers { - m[http.CanonicalHeaderKey(h)] = struct{}{} - } - result := make([]string, 0, len(m)) - for k := range m { - result = append(result, k) - } - - // Sort - sort.Strings(result) - return result -} - -// AddAccessControlHeaders adds default headers used for controlling -// cross-origin requests. This function adds several values to the -// Access-Control-Allow-Headers and Access-Control-Expose-Headers entries. -// If the Access-Control-Allow-Origin entry is missing a value of '*' is -// added, indicating that browsers should allow requesting code from any -// origin to access the resource. -// If the Access-Control-Allow-Methods entry is missing a value of 'GET' is -// added, indicating that browsers may use the GET method when issuing cross -// origin requests. -func AddAccessControlHeaders(headers map[string][]string) { - // Hard-coded headers. - const ACAHeadersName = "Access-Control-Allow-Headers" - const ACEHeadersName = "Access-Control-Expose-Headers" - const ACAOriginName = "Access-Control-Allow-Origin" - const ACAMethodsName = "Access-Control-Allow-Methods" - - if _, ok := headers[ACAOriginName]; !ok { - // Default to *all* - headers[ACAOriginName] = []string{"*"} - } - if _, ok := headers[ACAMethodsName]; !ok { - // Default to GET - headers[ACAMethodsName] = []string{http.MethodGet} - } - - headers[ACAHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Type", - "User-Agent", - "Range", - "X-Requested-With", - }, headers[ACAHeadersName]...)) - - headers[ACEHeadersName] = cleanHeaderSet( - append([]string{ - "Content-Length", - "Content-Range", - "X-Chunked-Output", - "X-Stream-Output", - "X-Ipfs-Path", - "X-Ipfs-Roots", - }, headers[ACEHeadersName]...)) -} - -type RequestContextKey string - -const ( - DNSLinkHostnameKey RequestContextKey = "dnslink-hostname" - GatewayHostnameKey RequestContextKey = "gw-hostname" -) diff --git a/extern/go-libipfs/gateway/handler.go b/extern/go-libipfs/gateway/handler.go deleted file mode 100644 index 97ceb8c02..000000000 --- a/extern/go-libipfs/gateway/handler.go +++ /dev/null @@ -1,929 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "html/template" - "io" - "mime" - "net/http" - "net/textproto" - "net/url" - gopath "path" - "regexp" - "runtime/debug" - "strings" - "time" - - cid "github.com/ipfs/go-cid" - ipld "github.com/ipfs/go-ipld-format" - logging "github.com/ipfs/go-log" - "github.com/ipfs/go-namesys" - "github.com/ipfs/go-path/resolver" - coreiface "github.com/ipfs/interface-go-ipfs-core" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - routing "github.com/libp2p/go-libp2p/core/routing" - mc "github.com/multiformats/go-multicodec" - prometheus "github.com/prometheus/client_golang/prometheus" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -var log = logging.Logger("core/server") - -const ( - ipfsPathPrefix = "/ipfs/" - ipnsPathPrefix = "/ipns/" - immutableCacheControl = "public, max-age=29030400, immutable" -) - -var ( - onlyASCII = regexp.MustCompile("[[:^ascii:]]") - noModtime = time.Unix(0, 0) // disables Last-Modified header if passed as modtime -) - -// HTML-based redirect for errors which can be recovered from, but we want -// to provide hint to people that they should fix things on their end. -var redirectTemplate = template.Must(template.New("redirect").Parse(` - - - - - - - -

{{.ErrorMsg}}
(if a redirect does not happen in 10 seconds, use "{{.SuggestedPath}}" instead)
- -`)) - -type redirectTemplateData struct { - RedirectURL string - SuggestedPath string - ErrorMsg string -} - -// handler is a HTTP handler that serves IPFS objects (accessible by default at /ipfs/) -// (it serves requests like GET /ipfs/QmVRzPKPzNtSrEzBFm2UZfxmPAgnaLke4DMcerbsGGSaFe/link) -type handler struct { - config Config - api API - - // generic metrics - firstContentBlockGetMetric *prometheus.HistogramVec - unixfsGetMetric *prometheus.SummaryVec // deprecated, use firstContentBlockGetMetric - - // response type metrics - getMetric *prometheus.HistogramVec - unixfsFileGetMetric *prometheus.HistogramVec - unixfsDirIndexGetMetric *prometheus.HistogramVec - unixfsGenDirListingGetMetric *prometheus.HistogramVec - carStreamGetMetric *prometheus.HistogramVec - rawBlockGetMetric *prometheus.HistogramVec - tarStreamGetMetric *prometheus.HistogramVec - jsoncborDocumentGetMetric *prometheus.HistogramVec - ipnsRecordGetMetric *prometheus.HistogramVec -} - -// StatusResponseWriter enables us to override HTTP Status Code passed to -// WriteHeader function inside of http.ServeContent. Decision is based on -// presence of HTTP Headers such as Location. -type statusResponseWriter struct { - http.ResponseWriter -} - -// Custom type for collecting error details to be handled by `webRequestError` -type requestError struct { - Message string - StatusCode int - Err error -} - -func (r *requestError) Error() string { - return r.Err.Error() -} - -func newRequestError(message string, err error, statusCode int) *requestError { - return &requestError{ - Message: message, - Err: err, - StatusCode: statusCode, - } -} - -func (sw *statusResponseWriter) WriteHeader(code int) { - // Check if we need to adjust Status Code to account for scheduled redirect - // This enables us to return payload along with HTTP 301 - // for subdomain redirect in web browsers while also returning body for cli - // tools which do not follow redirects by default (curl, wget). - redirect := sw.ResponseWriter.Header().Get("Location") - if redirect != "" && code == http.StatusOK { - code = http.StatusMovedPermanently - log.Debugw("subdomain redirect", "location", redirect, "status", code) - } - sw.ResponseWriter.WriteHeader(code) -} - -// ServeContent replies to the request using the content in the provided ReadSeeker -// and returns the status code written and any error encountered during a write. -// It wraps http.ServeContent which takes care of If-None-Match+Etag, -// Content-Length and range requests. -func ServeContent(w http.ResponseWriter, req *http.Request, name string, modtime time.Time, content io.ReadSeeker) (int, bool, error) { - ew := &errRecordingResponseWriter{ResponseWriter: w} - http.ServeContent(ew, req, name, modtime, content) - - // When we calculate some metrics we want a flag that lets us to ignore - // errors and 304 Not Modified, and only care when requested data - // was sent in full. - dataSent := ew.code/100 == 2 && ew.err == nil - - return ew.code, dataSent, ew.err -} - -// errRecordingResponseWriter wraps a ResponseWriter to record the status code and any write error. -type errRecordingResponseWriter struct { - http.ResponseWriter - code int - err error -} - -func (w *errRecordingResponseWriter) WriteHeader(code int) { - if w.code == 0 { - w.code = code - } - w.ResponseWriter.WriteHeader(code) -} - -func (w *errRecordingResponseWriter) Write(p []byte) (int, error) { - n, err := w.ResponseWriter.Write(p) - if err != nil && w.err == nil { - w.err = err - } - return n, err -} - -// ReadFrom exposes errRecordingResponseWriter's underlying ResponseWriter to io.Copy -// to allow optimized methods to be taken advantage of. -func (w *errRecordingResponseWriter) ReadFrom(r io.Reader) (n int64, err error) { - n, err = io.Copy(w.ResponseWriter, r) - if err != nil && w.err == nil { - w.err = err - } - return n, err -} - -func newSummaryMetric(name string, help string) *prometheus.SummaryVec { - summaryMetric := prometheus.NewSummaryVec( - prometheus.SummaryOpts{ - Namespace: "ipfs", - Subsystem: "http", - Name: name, - Help: help, - }, - []string{"gateway"}, - ) - if err := prometheus.Register(summaryMetric); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { - summaryMetric = are.ExistingCollector.(*prometheus.SummaryVec) - } else { - log.Errorf("failed to register ipfs_http_%s: %v", name, err) - } - } - return summaryMetric -} - -func newHistogramMetric(name string, help string) *prometheus.HistogramVec { - // We can add buckets as a parameter in the future, but for now using static defaults - // suggested in https://github.com/ipfs/kubo/issues/8441 - defaultBuckets := []float64{0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 30, 60} - histogramMetric := prometheus.NewHistogramVec( - prometheus.HistogramOpts{ - Namespace: "ipfs", - Subsystem: "http", - Name: name, - Help: help, - Buckets: defaultBuckets, - }, - []string{"gateway"}, - ) - if err := prometheus.Register(histogramMetric); err != nil { - if are, ok := err.(prometheus.AlreadyRegisteredError); ok { - histogramMetric = are.ExistingCollector.(*prometheus.HistogramVec) - } else { - log.Errorf("failed to register ipfs_http_%s: %v", name, err) - } - } - return histogramMetric -} - -// NewHandler returns an http.Handler that can act as a gateway to IPFS content -// offlineApi is a version of the API that should not make network requests for missing data -func NewHandler(c Config, api API) http.Handler { - return newHandler(c, api) -} - -func newHandler(c Config, api API) *handler { - i := &handler{ - config: c, - api: api, - // Improved Metrics - // ---------------------------- - // Time till the first content block (bar in /ipfs/cid/foo/bar) - // (format-agnostic, across all response types) - firstContentBlockGetMetric: newHistogramMetric( - "gw_first_content_block_get_latency_seconds", - "The time till the first content block is received on GET from the gateway.", - ), - - // Response-type specific metrics - // ---------------------------- - // Generic: time it takes to execute a successful gateway request (all request types) - getMetric: newHistogramMetric( - "gw_get_duration_seconds", - "The time to GET a successful response to a request (all content types).", - ), - // UnixFS: time it takes to return a file - unixfsFileGetMetric: newHistogramMetric( - "gw_unixfs_file_get_duration_seconds", - "The time to serve an entire UnixFS file from the gateway.", - ), - // UnixFS: time it takes to find and serve an index.html file on behalf of a directory. - unixfsDirIndexGetMetric: newHistogramMetric( - "gw_unixfs_dir_indexhtml_get_duration_seconds", - "The time to serve an index.html file on behalf of a directory from the gateway. This is a subset of gw_unixfs_file_get_duration_seconds.", - ), - // UnixFS: time it takes to generate static HTML with directory listing - unixfsGenDirListingGetMetric: newHistogramMetric( - "gw_unixfs_gen_dir_listing_get_duration_seconds", - "The time to serve a generated UnixFS HTML directory listing from the gateway.", - ), - // CAR: time it takes to return requested CAR stream - carStreamGetMetric: newHistogramMetric( - "gw_car_stream_get_duration_seconds", - "The time to GET an entire CAR stream from the gateway.", - ), - // Block: time it takes to return requested Block - rawBlockGetMetric: newHistogramMetric( - "gw_raw_block_get_duration_seconds", - "The time to GET an entire raw Block from the gateway.", - ), - // TAR: time it takes to return requested TAR stream - tarStreamGetMetric: newHistogramMetric( - "gw_tar_stream_get_duration_seconds", - "The time to GET an entire TAR stream from the gateway.", - ), - // JSON/CBOR: time it takes to return requested DAG-JSON/-CBOR document - jsoncborDocumentGetMetric: newHistogramMetric( - "gw_jsoncbor_get_duration_seconds", - "The time to GET an entire DAG-JSON/CBOR block from the gateway.", - ), - // IPNS Record: time it takes to return IPNS record - ipnsRecordGetMetric: newHistogramMetric( - "gw_ipns_record_get_duration_seconds", - "The time to GET an entire IPNS Record from the gateway.", - ), - - // Legacy Metrics - // ---------------------------- - unixfsGetMetric: newSummaryMetric( // TODO: remove? - // (deprecated, use firstContentBlockGetMetric instead) - "unixfs_get_latency_seconds", - "DEPRECATED: does not do what you think, use gw_first_content_block_get_latency_seconds instead.", - ), - } - return i -} - -func (i *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - // the hour is a hard fallback, we don't expect it to happen, but just in case - ctx, cancel := context.WithTimeout(r.Context(), time.Hour) - defer cancel() - r = r.WithContext(ctx) - - defer func() { - if r := recover(); r != nil { - log.Error("A panic occurred in the gateway handler!") - log.Error(r) - debug.PrintStack() - } - }() - - switch r.Method { - case http.MethodGet, http.MethodHead: - i.getOrHeadHandler(w, r) - return - case http.MethodOptions: - i.optionsHandler(w, r) - return - } - - w.Header().Add("Allow", http.MethodGet) - w.Header().Add("Allow", http.MethodHead) - w.Header().Add("Allow", http.MethodOptions) - - errmsg := "Method " + r.Method + " not allowed: read only access" - http.Error(w, errmsg, http.StatusMethodNotAllowed) -} - -func (i *handler) optionsHandler(w http.ResponseWriter, r *http.Request) { - /* - OPTIONS is a noop request that is used by the browsers to check - if server accepts cross-site XMLHttpRequest (indicated by the presence of CORS headers) - https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Preflighted_requests - */ - i.addUserHeaders(w) // return all custom headers (including CORS ones, if set) -} - -func (i *handler) getOrHeadHandler(w http.ResponseWriter, r *http.Request) { - begin := time.Now() - - logger := log.With("from", r.RequestURI) - logger.Debug("http request received") - - if err := handleUnsupportedHeaders(r); err != nil { - webRequestError(w, err) - return - } - - if requestHandled := handleProtocolHandlerRedirect(w, r, logger); requestHandled { - return - } - - if err := handleServiceWorkerRegistration(r); err != nil { - webRequestError(w, err) - return - } - - contentPath := ipath.New(r.URL.Path) - - if requestHandled := i.handleOnlyIfCached(w, r, contentPath, logger); requestHandled { - return - } - - if requestHandled := handleSuperfluousNamespace(w, r, contentPath); requestHandled { - return - } - - // Detect when explicit Accept header or ?format parameter are present - responseFormat, formatParams, err := customResponseFormat(r) - if err != nil { - webError(w, "error while processing the Accept header", err, http.StatusBadRequest) - return - } - trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResponseFormat", responseFormat)) - - resolvedPath, contentPath, ok := i.handlePathResolution(w, r, responseFormat, contentPath, logger) - if !ok { - return - } - trace.SpanFromContext(r.Context()).SetAttributes(attribute.String("ResolvedPath", resolvedPath.String())) - - // Detect when If-None-Match HTTP header allows returning HTTP 304 Not Modified - if inm := r.Header.Get("If-None-Match"); inm != "" { - pathCid := resolvedPath.Cid() - // need to check against both File and Dir Etag variants - // because this inexpensive check happens before we do any I/O - cidEtag := getEtag(r, pathCid) - dirEtag := getDirListingEtag(pathCid) - if etagMatch(inm, cidEtag, dirEtag) { - // Finish early if client already has a matching Etag - w.WriteHeader(http.StatusNotModified) - return - } - } - - if err := i.handleGettingFirstBlock(r, begin, contentPath, resolvedPath); err != nil { - webRequestError(w, err) - return - } - - if err := i.setCommonHeaders(w, r, contentPath); err != nil { - webRequestError(w, err) - return - } - - var success bool - - // Support custom response formats passed via ?format or Accept HTTP header - switch responseFormat { - case "", "application/json", "application/cbor": - switch mc.Code(resolvedPath.Cid().Prefix().Codec) { - case mc.Json, mc.DagJson, mc.Cbor, mc.DagCbor: - logger.Debugw("serving codec", "path", contentPath) - success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) - default: - logger.Debugw("serving unixfs", "path", contentPath) - success = i.serveUnixFS(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - } - case "application/vnd.ipld.raw": - logger.Debugw("serving raw block", "path", contentPath) - success = i.serveRawBlock(r.Context(), w, r, resolvedPath, contentPath, begin) - case "application/vnd.ipld.car": - logger.Debugw("serving car stream", "path", contentPath) - carVersion := formatParams["version"] - success = i.serveCAR(r.Context(), w, r, resolvedPath, contentPath, carVersion, begin) - case "application/x-tar": - logger.Debugw("serving tar file", "path", contentPath) - success = i.serveTAR(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - case "application/vnd.ipld.dag-json", "application/vnd.ipld.dag-cbor": - logger.Debugw("serving codec", "path", contentPath) - success = i.serveCodec(r.Context(), w, r, resolvedPath, contentPath, begin, responseFormat) - case "application/vnd.ipfs.ipns-record": - logger.Debugw("serving ipns record", "path", contentPath) - success = i.serveIpnsRecord(r.Context(), w, r, resolvedPath, contentPath, begin, logger) - default: // catch-all for unsuported application/vnd.* - err := fmt.Errorf("unsupported format %q", responseFormat) - webError(w, "failed to respond with requested content type", err, http.StatusBadRequest) - return - } - - if success { - i.getMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } -} - -func (i *handler) addUserHeaders(w http.ResponseWriter) { - for k, v := range i.config.Headers { - w.Header()[k] = v - } -} - -func addCacheControlHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, fileCid cid.Cid) (modtime time.Time) { - // Set Etag to based on CID (override whatever was set before) - w.Header().Set("Etag", getEtag(r, fileCid)) - - // Set Cache-Control and Last-Modified based on contentPath properties - if contentPath.Mutable() { - // mutable namespaces such as /ipns/ can't be cached forever - - /* For now we set Last-Modified to Now() to leverage caching heuristics built into modern browsers: - * https://github.com/ipfs/kubo/pull/8074#pullrequestreview-645196768 - * but we should not set it to fake values and use Cache-Control based on TTL instead */ - modtime = time.Now() - - // TODO: set Cache-Control based on TTL of IPNS/DNSLink: https://github.com/ipfs/kubo/issues/1818#issuecomment-1015849462 - // TODO: set Last-Modified based on /ipns/ publishing timestamp? - } else { - // immutable! CACHE ALL THE THINGS, FOREVER! wolololol - w.Header().Set("Cache-Control", immutableCacheControl) - - // Set modtime to 'zero time' to disable Last-Modified header (superseded by Cache-Control) - modtime = noModtime - - // TODO: set Last-Modified? - TBD - /ipfs/ modification metadata is present in unixfs 1.5 https://github.com/ipfs/kubo/issues/6920? - } - - return modtime -} - -// Set Content-Disposition if filename URL query param is present, return preferred filename -func addContentDispositionHeader(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) string { - /* This logic enables: - * - creation of HTML links that trigger "Save As.." dialog instead of being rendered by the browser - * - overriding the filename used when saving subresource assets on HTML page - * - providing a default filename for HTTP clients when downloading direct /ipfs/CID without any subpath - */ - - // URL param ?filename=cat.jpg triggers Content-Disposition: [..] filename - // which impacts default name used in "Save As.." dialog - name := getFilename(contentPath) - urlFilename := r.URL.Query().Get("filename") - if urlFilename != "" { - disposition := "inline" - // URL param ?download=true triggers Content-Disposition: [..] attachment - // which skips rendering and forces "Save As.." dialog in browsers - if r.URL.Query().Get("download") == "true" { - disposition = "attachment" - } - setContentDispositionHeader(w, urlFilename, disposition) - name = urlFilename - } - return name -} - -// Set Content-Disposition to arbitrary filename and disposition -func setContentDispositionHeader(w http.ResponseWriter, filename string, disposition string) { - utf8Name := url.PathEscape(filename) - asciiName := url.PathEscape(onlyASCII.ReplaceAllLiteralString(filename, "_")) - w.Header().Set("Content-Disposition", fmt.Sprintf("%s; filename=\"%s\"; filename*=UTF-8''%s", disposition, asciiName, utf8Name)) -} - -// Set X-Ipfs-Roots with logical CID array for efficient HTTP cache invalidation. -func (i *handler) buildIpfsRootsHeader(contentPath string, r *http.Request) (string, error) { - /* - These are logical roots where each CID represent one path segment - and resolves to either a directory or the root block of a file. - The main purpose of this header is allow HTTP caches to do smarter decisions - around cache invalidation (eg. keep specific subdirectory/file if it did not change) - - A good example is Wikipedia, which is HAMT-sharded, but we only care about - logical roots that represent each segment of the human-readable content - path: - - Given contentPath = /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey - rootCidList is a generated by doing `ipfs resolve -r` on each sub path: - /ipns/en.wikipedia-on-ipfs.org → bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze - /ipns/en.wikipedia-on-ipfs.org/wiki/ → bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4 - /ipns/en.wikipedia-on-ipfs.org/wiki/Block_of_Wikipedia_in_Turkey → bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma - - The result is an ordered array of values: - X-Ipfs-Roots: bafybeiaysi4s6lnjev27ln5icwm6tueaw2vdykrtjkwiphwekaywqhcjze,bafybeihn2f7lhumh4grizksi2fl233cyszqadkn424ptjajfenykpsaiw4,bafkreibn6euazfvoghepcm4efzqx5l3hieof2frhp254hio5y7n3hv5rma - - Note that while the top one will change every time any article is changed, - the last root (responsible for specific article) may not change at all. - */ - var sp strings.Builder - var pathRoots []string - pathSegments := strings.Split(contentPath[6:], "/") - sp.WriteString(contentPath[:5]) // /ipfs or /ipns - for _, root := range pathSegments { - if root == "" { - continue - } - sp.WriteString("/") - sp.WriteString(root) - resolvedSubPath, err := i.api.ResolvePath(r.Context(), ipath.New(sp.String())) - if err != nil { - return "", err - } - pathRoots = append(pathRoots, resolvedSubPath.Cid().String()) - } - rootCidList := strings.Join(pathRoots, ",") // convention from rfc2616#sec4.2 - return rootCidList, nil -} - -func webRequestError(w http.ResponseWriter, err *requestError) { - webError(w, err.Message, err.Err, err.StatusCode) -} - -func webError(w http.ResponseWriter, message string, err error, defaultCode int) { - if _, ok := err.(resolver.ErrNoLink); ok { - webErrorWithCode(w, message, err, http.StatusNotFound) - } else if err == routing.ErrNotFound { - webErrorWithCode(w, message, err, http.StatusNotFound) - } else if ipld.IsNotFound(err) { - webErrorWithCode(w, message, err, http.StatusNotFound) - } else if err == context.DeadlineExceeded { - webErrorWithCode(w, message, err, http.StatusRequestTimeout) - } else { - webErrorWithCode(w, message, err, defaultCode) - } -} - -func webErrorWithCode(w http.ResponseWriter, message string, err error, code int) { - http.Error(w, fmt.Sprintf("%s: %s", message, err), code) - if code >= 500 { - log.Warnf("server error: %s: %s", message, err) - } -} - -// return a 500 error and log -func internalWebError(w http.ResponseWriter, err error) { - webErrorWithCode(w, "internalWebError", err, http.StatusInternalServerError) -} - -func getFilename(contentPath ipath.Path) string { - s := contentPath.String() - if (strings.HasPrefix(s, ipfsPathPrefix) || strings.HasPrefix(s, ipnsPathPrefix)) && strings.Count(gopath.Clean(s), "/") <= 2 { - // Don't want to treat ipfs.io in /ipns/ipfs.io as a filename. - return "" - } - return gopath.Base(s) -} - -// etagMatch evaluates if we can respond with HTTP 304 Not Modified -// It supports multiple weak and strong etags passed in If-None-Matc stringh -// including the wildcard one. -func etagMatch(ifNoneMatchHeader string, cidEtag string, dirEtag string) bool { - buf := ifNoneMatchHeader - for { - buf = textproto.TrimString(buf) - if len(buf) == 0 { - break - } - if buf[0] == ',' { - buf = buf[1:] - continue - } - // If-None-Match: * should match against any etag - if buf[0] == '*' { - return true - } - etag, remain := scanETag(buf) - if etag == "" { - break - } - // Check for match both strong and weak etags - if etagWeakMatch(etag, cidEtag) || etagWeakMatch(etag, dirEtag) { - return true - } - buf = remain - } - return false -} - -// scanETag determines if a syntactically valid ETag is present at s. If so, -// the ETag and remaining text after consuming ETag is returned. Otherwise, -// it returns "", "". -// (This is the same logic as one executed inside of http.ServeContent) -func scanETag(s string) (etag string, remain string) { - s = textproto.TrimString(s) - start := 0 - if strings.HasPrefix(s, "W/") { - start = 2 - } - if len(s[start:]) < 2 || s[start] != '"' { - return "", "" - } - // ETag is either W/"text" or "text". - // See RFC 7232 2.3. - for i := start + 1; i < len(s); i++ { - c := s[i] - switch { - // Character values allowed in ETags. - case c == 0x21 || c >= 0x23 && c <= 0x7E || c >= 0x80: - case c == '"': - return s[:i+1], s[i+1:] - default: - return "", "" - } - } - return "", "" -} - -// etagWeakMatch reports whether a and b match using weak ETag comparison. -func etagWeakMatch(a, b string) bool { - return strings.TrimPrefix(a, "W/") == strings.TrimPrefix(b, "W/") -} - -// generate Etag value based on HTTP request and CID -func getEtag(r *http.Request, cid cid.Cid) string { - prefix := `"` - suffix := `"` - responseFormat, _, err := customResponseFormat(r) - if err == nil && responseFormat != "" { - // application/vnd.ipld.foo → foo - // application/x-bar → x-bar - shortFormat := responseFormat[strings.LastIndexAny(responseFormat, "/.")+1:] - // Etag: "cid.shortFmt" (gives us nice compression together with Content-Disposition in block (raw) and car responses) - suffix = `.` + shortFormat + suffix - } - // TODO: include selector suffix when https://github.com/ipfs/kubo/issues/8769 lands - return prefix + cid.String() + suffix -} - -// return explicit response format if specified in request as query parameter or via Accept HTTP header -func customResponseFormat(r *http.Request) (mediaType string, params map[string]string, err error) { - if formatParam := r.URL.Query().Get("format"); formatParam != "" { - // translate query param to a content type - switch formatParam { - case "raw": - return "application/vnd.ipld.raw", nil, nil - case "car": - return "application/vnd.ipld.car", nil, nil - case "tar": - return "application/x-tar", nil, nil - case "json": - return "application/json", nil, nil - case "cbor": - return "application/cbor", nil, nil - case "dag-json": - return "application/vnd.ipld.dag-json", nil, nil - case "dag-cbor": - return "application/vnd.ipld.dag-cbor", nil, nil - case "ipns-record": - return "application/vnd.ipfs.ipns-record", nil, nil - } - } - // Browsers and other user agents will send Accept header with generic types like: - // Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8 - // We only care about explicit, vendor-specific content-types and respond to the first match (in order). - // TODO: make this RFC compliant and respect weights (eg. return CAR for Accept:application/vnd.ipld.dag-json;q=0.1,application/vnd.ipld.car;q=0.2) - for _, header := range r.Header.Values("Accept") { - for _, value := range strings.Split(header, ",") { - accept := strings.TrimSpace(value) - // respond to the very first matching content type - if strings.HasPrefix(accept, "application/vnd.ipld") || - strings.HasPrefix(accept, "application/x-tar") || - strings.HasPrefix(accept, "application/json") || - strings.HasPrefix(accept, "application/cbor") || - strings.HasPrefix(accept, "application/vnd.ipfs") { - mediatype, params, err := mime.ParseMediaType(accept) - if err != nil { - return "", nil, err - } - return mediatype, params, nil - } - } - } - // If none of special-cased content types is found, return empty string - // to indicate default, implicit UnixFS response should be prepared - return "", nil, nil -} - -// returns unquoted path with all special characters revealed as \u codes -func debugStr(path string) string { - q := fmt.Sprintf("%+q", path) - if len(q) >= 3 { - q = q[1 : len(q)-1] - } - return q -} - -// Resolve the provided contentPath including any special handling related to -// the requested responseFormat. Returned ok flag indicates if gateway handler -// should continue processing the request. -func (i *handler) handlePathResolution(w http.ResponseWriter, r *http.Request, responseFormat string, contentPath ipath.Path, logger *zap.SugaredLogger) (resolvedPath ipath.Resolved, newContentPath ipath.Path, ok bool) { - // Attempt to resolve the provided path. - resolvedPath, err := i.api.ResolvePath(r.Context(), contentPath) - - switch err { - case nil: - return resolvedPath, contentPath, true - case coreiface.ErrOffline: - webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusServiceUnavailable) - return nil, nil, false - case namesys.ErrResolveFailed: - // Note: webError will replace http.StatusBadRequest with StatusNotFound or StatusRequestTimeout if necessary - webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusInternalServerError) - return nil, nil, false - default: - // The path can't be resolved. - if isUnixfsResponseFormat(responseFormat) { - // If we have origin isolation (subdomain gw, DNSLink website), - // and response type is UnixFS (default for website hosting) - // check for presence of _redirects file and apply rules defined there. - // See: https://github.com/ipfs/specs/pull/290 - if hasOriginIsolation(r) { - resolvedPath, newContentPath, ok, hadMatchingRule := i.serveRedirectsIfPresent(w, r, resolvedPath, contentPath, logger) - if hadMatchingRule { - logger.Debugw("applied a rule from _redirects file") - return resolvedPath, newContentPath, ok - } - } - - // if Accept is text/html, see if ipfs-404.html is present - // This logic isn't documented and will likely be removed at some point. - // Any 404 logic in _redirects above will have already run by this time, so it's really an extra fall back - if i.serveLegacy404IfPresent(w, r, contentPath) { - logger.Debugw("served legacy 404") - return nil, nil, false - } - } - - // Note: webError will replace http.StatusBadRequest with StatusNotFound or StatusRequestTimeout if necessary - webError(w, "ipfs resolve -r "+debugStr(contentPath.String()), err, http.StatusBadRequest) - return nil, nil, false - } -} - -// Detect 'Cache-Control: only-if-cached' in request and return data if it is already in the local datastore. -// https://github.com/ipfs/specs/blob/main/http-gateways/PATH_GATEWAY.md#cache-control-request-header -func (i *handler) handleOnlyIfCached(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) (requestHandled bool) { - if r.Header.Get("Cache-Control") == "only-if-cached" { - if !i.api.IsCached(r.Context(), contentPath) { - if r.Method == http.MethodHead { - w.WriteHeader(http.StatusPreconditionFailed) - return true - } - errMsg := fmt.Sprintf("%q not in local datastore", contentPath.String()) - http.Error(w, errMsg, http.StatusPreconditionFailed) - return true - } - if r.Method == http.MethodHead { - w.WriteHeader(http.StatusOK) - return true - } - } - return false -} - -func handleUnsupportedHeaders(r *http.Request) (err *requestError) { - // X-Ipfs-Gateway-Prefix was removed (https://github.com/ipfs/kubo/issues/7702) - // TODO: remove this after go-ipfs 0.13 ships - if prfx := r.Header.Get("X-Ipfs-Gateway-Prefix"); prfx != "" { - err := fmt.Errorf("X-Ipfs-Gateway-Prefix support was removed: https://github.com/ipfs/kubo/issues/7702") - return newRequestError("unsupported HTTP header", err, http.StatusBadRequest) - } - return nil -} - -// ?uri query param support for requests produced by web browsers -// via navigator.registerProtocolHandler Web API -// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/registerProtocolHandler -// TLDR: redirect /ipfs/?uri=ipfs%3A%2F%2Fcid%3Fquery%3Dval to /ipfs/cid?query=val -func handleProtocolHandlerRedirect(w http.ResponseWriter, r *http.Request, logger *zap.SugaredLogger) (requestHandled bool) { - if uriParam := r.URL.Query().Get("uri"); uriParam != "" { - u, err := url.Parse(uriParam) - if err != nil { - webError(w, "failed to parse uri query parameter", err, http.StatusBadRequest) - return true - } - if u.Scheme != "ipfs" && u.Scheme != "ipns" { - webError(w, "uri query parameter scheme must be ipfs or ipns", err, http.StatusBadRequest) - return true - } - path := u.Path - if u.RawQuery != "" { // preserve query if present - path = path + "?" + u.RawQuery - } - - redirectURL := gopath.Join("/", u.Scheme, u.Host, path) - logger.Debugw("uri param, redirect", "to", redirectURL, "status", http.StatusMovedPermanently) - http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) - return true - } - - return false -} - -// Disallow Service Worker registration on namespace roots -// https://github.com/ipfs/kubo/issues/4025 -func handleServiceWorkerRegistration(r *http.Request) (err *requestError) { - if r.Header.Get("Service-Worker") == "script" { - matched, _ := regexp.MatchString(`^/ip[fn]s/[^/]+$`, r.URL.Path) - if matched { - err := fmt.Errorf("registration is not allowed for this scope") - return newRequestError("navigator.serviceWorker", err, http.StatusBadRequest) - } - } - - return nil -} - -// Attempt to fix redundant /ipfs/ namespace as long as resulting -// 'intended' path is valid. This is in case gremlins were tickled -// wrong way and user ended up at /ipfs/ipfs/{cid} or /ipfs/ipns/{id} -// like in bafybeien3m7mdn6imm425vc2s22erzyhbvk5n3ofzgikkhmdkh5cuqbpbq :^)) -func handleSuperfluousNamespace(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) (requestHandled bool) { - // If the path is valid, there's nothing to do - if pathErr := contentPath.IsValid(); pathErr == nil { - return false - } - - // If there's no superflous namespace, there's nothing to do - if !(strings.HasPrefix(r.URL.Path, "/ipfs/ipfs/") || strings.HasPrefix(r.URL.Path, "/ipfs/ipns/")) { - return false - } - - // Attempt to fix the superflous namespace - intendedPath := ipath.New(strings.TrimPrefix(r.URL.Path, "/ipfs")) - if err := intendedPath.IsValid(); err != nil { - webError(w, "invalid ipfs path", err, http.StatusBadRequest) - return true - } - intendedURL := intendedPath.String() - if r.URL.RawQuery != "" { - // we render HTML, so ensure query entries are properly escaped - q, _ := url.ParseQuery(r.URL.RawQuery) - intendedURL = intendedURL + "?" + q.Encode() - } - // return HTTP 400 (Bad Request) with HTML error page that: - // - points at correct canonical path via header - // - displays human-readable error - // - redirects to intendedURL after a short delay - - w.WriteHeader(http.StatusBadRequest) - if err := redirectTemplate.Execute(w, redirectTemplateData{ - RedirectURL: intendedURL, - SuggestedPath: intendedPath.String(), - ErrorMsg: fmt.Sprintf("invalid path: %q should be %q", r.URL.Path, intendedPath.String()), - }); err != nil { - webError(w, "failed to redirect when fixing superfluous namespace", err, http.StatusBadRequest) - } - - return true -} - -func (i *handler) handleGettingFirstBlock(r *http.Request, begin time.Time, contentPath ipath.Path, resolvedPath ipath.Resolved) *requestError { - // Update the global metric of the time it takes to read the final root block of the requested resource - // NOTE: for legacy reasons this happens before we go into content-type specific code paths - _, err := i.api.GetBlock(r.Context(), resolvedPath.Cid()) - if err != nil { - return newRequestError("ipfs block get "+resolvedPath.Cid().String(), err, http.StatusInternalServerError) - } - ns := contentPath.Namespace() - timeToGetFirstContentBlock := time.Since(begin).Seconds() - i.unixfsGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) // deprecated, use firstContentBlockGetMetric instead - i.firstContentBlockGetMetric.WithLabelValues(ns).Observe(timeToGetFirstContentBlock) - return nil -} - -func (i *handler) setCommonHeaders(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) *requestError { - i.addUserHeaders(w) // ok, _now_ write user's headers. - w.Header().Set("X-Ipfs-Path", contentPath.String()) - - if rootCids, err := i.buildIpfsRootsHeader(contentPath.String(), r); err == nil { - w.Header().Set("X-Ipfs-Roots", rootCids) - } else { // this should never happen, as we resolved the contentPath already - return newRequestError("error while resolving X-Ipfs-Roots", err, http.StatusInternalServerError) - } - - return nil -} - -// spanTrace starts a new span using the standard IPFS tracing conventions. -func spanTrace(ctx context.Context, spanName string, opts ...trace.SpanStartOption) (context.Context, trace.Span) { - return otel.Tracer("go-libipfs").Start(ctx, fmt.Sprintf("%s.%s", " Gateway", spanName), opts...) -} diff --git a/extern/go-libipfs/gateway/handler_block.go b/extern/go-libipfs/gateway/handler_block.go deleted file mode 100644 index fcb2408bc..000000000 --- a/extern/go-libipfs/gateway/handler_block.go +++ /dev/null @@ -1,51 +0,0 @@ -package gateway - -import ( - "bytes" - "context" - "net/http" - "time" - - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// serveRawBlock returns bytes behind a raw block -func (i *handler) serveRawBlock(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time) bool { - ctx, span := spanTrace(ctx, "ServeRawBlock", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - blockCid := resolvedPath.Cid() - block, err := i.api.GetBlock(ctx, blockCid) - if err != nil { - webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError) - return false - } - content := bytes.NewReader(block.RawData()) - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = blockCid.String() + ".bin" - } - setContentDispositionHeader(w, name, "attachment") - - // Set remaining headers - modtime := addCacheControlHeaders(w, r, contentPath, blockCid) - w.Header().Set("Content-Type", "application/vnd.ipld.raw") - w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) - - // ServeContent will take care of - // If-None-Match+Etag, Content-Length and range requests - _, dataSent, _ := ServeContent(w, r, name, modtime, content) - - if dataSent { - // Update metrics - i.rawBlockGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } - - return dataSent -} diff --git a/extern/go-libipfs/gateway/handler_car.go b/extern/go-libipfs/gateway/handler_car.go deleted file mode 100644 index b900d699a..000000000 --- a/extern/go-libipfs/gateway/handler_car.go +++ /dev/null @@ -1,99 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "net/http" - "time" - - cid "github.com/ipfs/go-cid" - blocks "github.com/ipfs/go-libipfs/blocks" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - gocar "github.com/ipld/go-car" - selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// serveCAR returns a CAR stream for specific DAG+selector -func (i *handler) serveCAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, carVersion string, begin time.Time) bool { - ctx, span := spanTrace(ctx, "ServeCAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - switch carVersion { - case "": // noop, client does not care about version - case "1": // noop, we support this - default: - err := fmt.Errorf("only version=1 is supported") - webError(w, "unsupported CAR version", err, http.StatusBadRequest) - return false - } - rootCid := resolvedPath.Cid() - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = rootCid.String() + ".car" - } - setContentDispositionHeader(w, name, "attachment") - - // Set Cache-Control (same logic as for a regular files) - addCacheControlHeaders(w, r, contentPath, rootCid) - - // Weak Etag W/ because we can't guarantee byte-for-byte identical - // responses, but still want to benefit from HTTP Caching. Two CAR - // responses for the same CID and selector will be logically equivalent, - // but when CAR is streamed, then in theory, blocks may arrive from - // datastore in non-deterministic order. - etag := `W/` + getEtag(r, rootCid) - w.Header().Set("Etag", etag) - - // Finish early if Etag match - if r.Header.Get("If-None-Match") == etag { - w.WriteHeader(http.StatusNotModified) - return false - } - - // Make it clear we don't support range-requests over a car stream - // Partial downloads and resumes should be handled using requests for - // sub-DAGs and IPLD selectors: https://github.com/ipfs/go-ipfs/issues/8769 - w.Header().Set("Accept-Ranges", "none") - - w.Header().Set("Content-Type", "application/vnd.ipld.car; version=1") - w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) - - // Same go-car settings as dag.export command - store := dagStore{api: i.api, ctx: ctx} - - // TODO: support selectors passed as request param: https://github.com/ipfs/kubo/issues/8769 - dag := gocar.Dag{Root: rootCid, Selector: selectorparse.CommonSelector_ExploreAllRecursively} - car := gocar.NewSelectiveCar(ctx, store, []gocar.Dag{dag}, gocar.TraverseLinksOnlyOnce()) - - if err := car.Write(w); err != nil { - // We return error as a trailer, however it is not something browsers can access - // (https://github.com/mdn/browser-compat-data/issues/14703) - // Due to this, we suggest client always verify that - // the received CAR stream response is matching requested DAG selector - w.Header().Set("X-Stream-Error", err.Error()) - return false - } - - // Update metrics - i.carStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - return true -} - -// FIXME(@Jorropo): https://github.com/ipld/go-car/issues/315 -type dagStore struct { - api API - ctx context.Context -} - -func (ds dagStore) Get(_ context.Context, c cid.Cid) (blocks.Block, error) { - return ds.api.GetBlock(ds.ctx, c) -} diff --git a/extern/go-libipfs/gateway/handler_codec.go b/extern/go-libipfs/gateway/handler_codec.go deleted file mode 100644 index 6c067e58f..000000000 --- a/extern/go-libipfs/gateway/handler_codec.go +++ /dev/null @@ -1,269 +0,0 @@ -package gateway - -import ( - "bytes" - "context" - "fmt" - "html" - "net/http" - "strings" - "time" - - "github.com/filecoin-project/boost/extern/go-libipfs/gateway/assets" - cid "github.com/ipfs/go-cid" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "github.com/ipld/go-ipld-prime/multicodec" - "github.com/ipld/go-ipld-prime/node/basicnode" - mc "github.com/multiformats/go-multicodec" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// codecToContentType maps the supported IPLD codecs to the HTTP Content -// Type they should have. -var codecToContentType = map[mc.Code]string{ - mc.Json: "application/json", - mc.Cbor: "application/cbor", - mc.DagJson: "application/vnd.ipld.dag-json", - mc.DagCbor: "application/vnd.ipld.dag-cbor", -} - -// contentTypeToRaw maps the HTTP Content Type to the respective codec that -// allows raw response without any conversion. -var contentTypeToRaw = map[string][]mc.Code{ - "application/json": {mc.Json, mc.DagJson}, - "application/cbor": {mc.Cbor, mc.DagCbor}, -} - -// contentTypeToCodec maps the HTTP Content Type to the respective codec. We -// only add here the codecs that we want to convert-to-from. -var contentTypeToCodec = map[string]mc.Code{ - "application/vnd.ipld.dag-json": mc.DagJson, - "application/vnd.ipld.dag-cbor": mc.DagCbor, -} - -// contentTypeToExtension maps the HTTP Content Type to the respective file -// extension, used in Content-Disposition header when downloading the file. -var contentTypeToExtension = map[string]string{ - "application/json": ".json", - "application/vnd.ipld.dag-json": ".json", - "application/cbor": ".cbor", - "application/vnd.ipld.dag-cbor": ".cbor", -} - -func (i *handler) serveCodec(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, requestedContentType string) bool { - ctx, span := spanTrace(ctx, "ServeCodec", trace.WithAttributes(attribute.String("path", resolvedPath.String()), attribute.String("requestedContentType", requestedContentType))) - defer span.End() - - cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec) - responseContentType := requestedContentType - - // If the resolved path still has some remainder, return error for now. - // TODO: handle this when we have IPLD Patch (https://ipld.io/specs/patch/) via HTTP PUT - // TODO: (depends on https://github.com/ipfs/kubo/issues/4801 and https://github.com/ipfs/kubo/issues/4782) - if resolvedPath.Remainder() != "" { - path := strings.TrimSuffix(resolvedPath.String(), resolvedPath.Remainder()) - err := fmt.Errorf("%q of %q could not be returned: reading IPLD Kinds other than Links (CBOR Tag 42) is not implemented: try reading %q instead", resolvedPath.Remainder(), resolvedPath.String(), path) - webError(w, "unsupported pathing", err, http.StatusNotImplemented) - return false - } - - // If no explicit content type was requested, the response will have one based on the codec from the CID - if requestedContentType == "" { - cidContentType, ok := codecToContentType[cidCodec] - if !ok { - // Should not happen unless function is called with wrong parameters. - err := fmt.Errorf("content type not found for codec: %v", cidCodec) - webError(w, "internal error", err, http.StatusInternalServerError) - return false - } - responseContentType = cidContentType - } - - // Set HTTP headers (for caching etc) - modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid()) - name := setCodecContentDisposition(w, r, resolvedPath, responseContentType) - w.Header().Set("Content-Type", responseContentType) - w.Header().Set("X-Content-Type-Options", "nosniff") - - // No content type is specified by the user (via Accept, or format=). However, - // we support this format. Let's handle it. - if requestedContentType == "" { - isDAG := cidCodec == mc.DagJson || cidCodec == mc.DagCbor - acceptsHTML := strings.Contains(r.Header.Get("Accept"), "text/html") - download := r.URL.Query().Get("download") == "true" - - if isDAG && acceptsHTML && !download { - return i.serveCodecHTML(ctx, w, r, resolvedPath, contentPath) - } else { - // This covers CIDs with codec 'json' and 'cbor' as those do not have - // an explicit requested content type. - return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin) - } - } - - // If DAG-JSON or DAG-CBOR was requested using corresponding plain content type - // return raw block as-is, without conversion - skipCodecs, ok := contentTypeToRaw[requestedContentType] - if ok { - for _, skipCodec := range skipCodecs { - if skipCodec == cidCodec { - return i.serveCodecRaw(ctx, w, r, resolvedPath, contentPath, name, modtime, begin) - } - } - } - - // Otherwise, the user has requested a specific content type (a DAG-* variant). - // Let's first get the codecs that can be used with this content type. - toCodec, ok := contentTypeToCodec[requestedContentType] - if !ok { - // This is never supposed to happen unless function is called with wrong parameters. - err := fmt.Errorf("unsupported content type: %s", requestedContentType) - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - // This handles DAG-* conversions and validations. - return i.serveCodecConverted(ctx, w, r, resolvedPath, contentPath, toCodec, modtime, begin) -} - -func (i *handler) serveCodecHTML(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path) bool { - // A HTML directory index will be presented, be sure to set the correct - // type instead of relying on autodetection (which may fail). - w.Header().Set("Content-Type", "text/html") - - // Clear Content-Disposition -- we want HTML to be rendered inline - w.Header().Del("Content-Disposition") - - // Generated index requires custom Etag (output may change between Kubo versions) - dagEtag := getDagIndexEtag(resolvedPath.Cid()) - w.Header().Set("Etag", dagEtag) - - // Remove Cache-Control for now to match UnixFS dir-index-html responses - // (we don't want browser to cache HTML forever) - // TODO: if we ever change behavior for UnixFS dir listings, same changes should be applied here - w.Header().Del("Cache-Control") - - cidCodec := mc.Code(resolvedPath.Cid().Prefix().Codec) - if err := assets.DagTemplate.Execute(w, assets.DagTemplateData{ - Path: contentPath.String(), - CID: resolvedPath.Cid().String(), - CodecName: cidCodec.String(), - CodecHex: fmt.Sprintf("0x%x", uint64(cidCodec)), - }); err != nil { - webError(w, "failed to generate HTML listing for this DAG: try fetching raw block with ?format=raw", err, http.StatusInternalServerError) - return false - } - - return true -} - -// serveCodecRaw returns the raw block without any conversion -func (i *handler) serveCodecRaw(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, name string, modtime, begin time.Time) bool { - blockCid := resolvedPath.Cid() - block, err := i.api.GetBlock(ctx, blockCid) - if err != nil { - webError(w, "ipfs block get "+blockCid.String(), err, http.StatusInternalServerError) - return false - } - content := bytes.NewReader(block.RawData()) - - // ServeContent will take care of - // If-None-Match+Etag, Content-Length and range requests - _, dataSent, _ := ServeContent(w, r, name, modtime, content) - - if dataSent { - // Update metrics - i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } - - return dataSent -} - -// serveCodecConverted returns payload converted to codec specified in toCodec -func (i *handler) serveCodecConverted(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, toCodec mc.Code, modtime, begin time.Time) bool { - blockCid := resolvedPath.Cid() - block, err := i.api.GetBlock(ctx, blockCid) - if err != nil { - webError(w, "ipfs block get "+html.EscapeString(resolvedPath.String()), err, http.StatusInternalServerError) - return false - } - - codec := blockCid.Prefix().Codec - decoder, err := multicodec.LookupDecoder(codec) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - node := basicnode.Prototype.Any.NewBuilder() - err = decoder(node, bytes.NewReader(block.RawData())) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - encoder, err := multicodec.LookupEncoder(uint64(toCodec)) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - // Ensure IPLD node conforms to the codec specification. - var buf bytes.Buffer - err = encoder(node.Build(), &buf) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - // Sets correct Last-Modified header. This code is borrowed from the standard - // library (net/http/server.go) as we cannot use serveFile. - if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) { - w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) - } - - _, err = w.Write(buf.Bytes()) - if err == nil { - // Update metrics - i.jsoncborDocumentGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - return true - } - - return false -} - -func setCodecContentDisposition(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentType string) string { - var dispType, name string - - ext, ok := contentTypeToExtension[contentType] - if !ok { - // Should never happen. - ext = ".bin" - } - - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = resolvedPath.Cid().String() + ext - } - - // JSON should be inlined, but ?download=true should still override - if r.URL.Query().Get("download") == "true" { - dispType = "attachment" - } else { - switch ext { - case ".json": // codecs that serialize to JSON can be rendered by browsers - dispType = "inline" - default: // everything else is assumed binary / opaque bytes - dispType = "attachment" - } - } - - setContentDispositionHeader(w, name, dispType) - return name -} - -func getDagIndexEtag(dagCid cid.Cid) string { - return `"DagIndex-` + assets.AssetHash + `_CID-` + dagCid.String() + `"` -} diff --git a/extern/go-libipfs/gateway/handler_ipns_record.go b/extern/go-libipfs/gateway/handler_ipns_record.go deleted file mode 100644 index e2f658579..000000000 --- a/extern/go-libipfs/gateway/handler_ipns_record.go +++ /dev/null @@ -1,90 +0,0 @@ -package gateway - -import ( - "context" - "errors" - "fmt" - "net/http" - "strings" - "time" - - "github.com/gogo/protobuf/proto" - "github.com/ipfs/go-cid" - ipns_pb "github.com/ipfs/go-ipns/pb" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -func (i *handler) serveIpnsRecord(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { - ctx, span := spanTrace(ctx, "ServeIPNSRecord", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - if contentPath.Namespace() != "ipns" { - err := fmt.Errorf("%s is not an IPNS link", contentPath.String()) - webError(w, err.Error(), err, http.StatusBadRequest) - return false - } - - key := contentPath.String() - key = strings.TrimSuffix(key, "/") - key = strings.TrimPrefix(key, "/ipns/") - if strings.Count(key, "/") != 0 { - err := errors.New("cannot find ipns key for subpath") - webError(w, err.Error(), err, http.StatusBadRequest) - return false - } - - c, err := cid.Decode(key) - if err != nil { - webError(w, err.Error(), err, http.StatusBadRequest) - return false - } - - rawRecord, err := i.api.GetIPNSRecord(ctx, c) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - var record ipns_pb.IpnsEntry - err = proto.Unmarshal(rawRecord, &record) - if err != nil { - webError(w, err.Error(), err, http.StatusInternalServerError) - return false - } - - // Set cache control headers based on the TTL set in the IPNS record. If the - // TTL is not present, we use the Last-Modified tag. We are tracking IPNS - // caching on: https://github.com/ipfs/kubo/issues/1818. - // TODO: use addCacheControlHeaders once #1818 is fixed. - w.Header().Set("Etag", getEtag(r, resolvedPath.Cid())) - if record.Ttl != nil { - seconds := int(time.Duration(*record.Ttl).Seconds()) - w.Header().Set("Cache-Control", fmt.Sprintf("public, max-age=%d", seconds)) - } else { - w.Header().Set("Last-Modified", time.Now().UTC().Format(http.TimeFormat)) - } - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = key + ".ipns-record" - } - setContentDispositionHeader(w, name, "attachment") - - w.Header().Set("Content-Type", "application/vnd.ipfs.ipns-record") - w.Header().Set("X-Content-Type-Options", "nosniff") - - _, err = w.Write(rawRecord) - if err == nil { - // Update metrics - i.ipnsRecordGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - return true - } - - return false -} diff --git a/extern/go-libipfs/gateway/handler_tar.go b/extern/go-libipfs/gateway/handler_tar.go deleted file mode 100644 index 9c7026d33..000000000 --- a/extern/go-libipfs/gateway/handler_tar.go +++ /dev/null @@ -1,95 +0,0 @@ -package gateway - -import ( - "context" - "html" - "net/http" - "time" - - "github.com/ipfs/go-libipfs/files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -var unixEpochTime = time.Unix(0, 0) - -func (i *handler) serveTAR(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { - ctx, span := spanTrace(ctx, "ServeTAR", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - // Get Unixfs file - file, err := i.api.GetUnixFsNode(ctx, resolvedPath) - if err != nil { - webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest) - return false - } - defer file.Close() - - rootCid := resolvedPath.Cid() - - // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, contentPath, rootCid) - - // Weak Etag W/ because we can't guarantee byte-for-byte identical - // responses, but still want to benefit from HTTP Caching. Two TAR - // responses for the same CID will be logically equivalent, - // but when TAR is streamed, then in theory, files and directories - // may arrive in different order (depends on TAR lib and filesystem/inodes). - etag := `W/` + getEtag(r, rootCid) - w.Header().Set("Etag", etag) - - // Finish early if Etag match - if r.Header.Get("If-None-Match") == etag { - w.WriteHeader(http.StatusNotModified) - return false - } - - // Set Content-Disposition - var name string - if urlFilename := r.URL.Query().Get("filename"); urlFilename != "" { - name = urlFilename - } else { - name = rootCid.String() + ".tar" - } - setContentDispositionHeader(w, name, "attachment") - - // Construct the TAR writer - tarw, err := files.NewTarWriter(w) - if err != nil { - webError(w, "could not build tar writer", err, http.StatusInternalServerError) - return false - } - defer tarw.Close() - - // Sets correct Last-Modified header. This code is borrowed from the standard - // library (net/http/server.go) as we cannot use serveFile without throwing the entire - // TAR into the memory first. - if !(modtime.IsZero() || modtime.Equal(unixEpochTime)) { - w.Header().Set("Last-Modified", modtime.UTC().Format(http.TimeFormat)) - } - - w.Header().Set("Content-Type", "application/x-tar") - w.Header().Set("X-Content-Type-Options", "nosniff") // no funny business in the browsers :^) - - // The TAR has a top-level directory (or file) named by the CID. - if err := tarw.WriteFile(file, rootCid.String()); err != nil { - w.Header().Set("X-Stream-Error", err.Error()) - // Trailer headers do not work in web browsers - // (see https://github.com/mdn/browser-compat-data/issues/14703) - // and we have limited options around error handling in browser contexts. - // To improve UX/DX, we finish response stream with error message, allowing client to - // (1) detect error by having corrupted TAR - // (2) be able to reason what went wrong by instecting the tail of TAR stream - _, _ = w.Write([]byte(err.Error())) - return false - } - - // Update metrics - i.tarStreamGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - return true -} diff --git a/extern/go-libipfs/gateway/handler_unixfs.go b/extern/go-libipfs/gateway/handler_unixfs.go deleted file mode 100644 index 7d6d52b3a..000000000 --- a/extern/go-libipfs/gateway/handler_unixfs.go +++ /dev/null @@ -1,44 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "html" - "net/http" - "time" - - "github.com/ipfs/go-ipfs-files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -func (i *handler) serveUnixFS(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, begin time.Time, logger *zap.SugaredLogger) bool { - ctx, span := spanTrace(ctx, "ServeUnixFS", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - // Handling UnixFS - dr, err := i.api.GetUnixFsNode(ctx, resolvedPath) - if err != nil { - webError(w, "ipfs cat "+html.EscapeString(contentPath.String()), err, http.StatusBadRequest) - return false - } - defer dr.Close() - - // Handling Unixfs file - if f, ok := dr.(files.File); ok { - logger.Debugw("serving unixfs file", "path", contentPath) - return i.serveFile(ctx, w, r, resolvedPath, contentPath, f, begin) - } - - // Handling Unixfs directory - dir, ok := dr.(files.Directory) - if !ok { - internalWebError(w, fmt.Errorf("unsupported UnixFS type")) - return false - } - - logger.Debugw("serving unixfs directory", "path", contentPath) - return i.serveDirectory(ctx, w, r, resolvedPath, contentPath, dir, begin, logger) -} diff --git a/extern/go-libipfs/gateway/handler_unixfs__redirects.go b/extern/go-libipfs/gateway/handler_unixfs__redirects.go deleted file mode 100644 index eb07da12f..000000000 --- a/extern/go-libipfs/gateway/handler_unixfs__redirects.go +++ /dev/null @@ -1,287 +0,0 @@ -package gateway - -import ( - "fmt" - "io" - "net/http" - gopath "path" - "strconv" - "strings" - - "github.com/filecoin-project/boost/extern/go-libipfs/files" - redirects "github.com/ipfs/go-ipfs-redirects-file" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.uber.org/zap" -) - -// Resolving a UnixFS path involves determining if the provided `path.Path` exists and returning the `path.Resolved` -// corresponding to that path. For UnixFS, path resolution is more involved. -// -// When a path under requested CID does not exist, Gateway will check if a `_redirects` file exists -// underneath the root CID of the path, and apply rules defined there. -// See sepcification introduced in: https://github.com/ipfs/specs/pull/290 -// -// Scenario 1: -// If a path exists, we always return the `path.Resolved` corresponding to that path, regardless of the existence of a `_redirects` file. -// -// Scenario 2: -// If a path does not exist, usually we should return a `nil` resolution path and an error indicating that the path -// doesn't exist. However, a `_redirects` file may exist and contain a redirect rule that redirects that path to a different path. -// We need to evaluate the rule and perform the redirect if present. -// -// Scenario 3: -// Another possibility is that the path corresponds to a rewrite rule (i.e. a rule with a status of 200). -// In this case, we don't perform a redirect, but do need to return a `path.Resolved` and `path.Path` corresponding to -// the rewrite destination path. -// -// Note that for security reasons, redirect rules are only processed when the request has origin isolation. -// See https://github.com/ipfs/specs/pull/290 for more information. -func (i *handler) serveRedirectsIfPresent(w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, logger *zap.SugaredLogger) (newResolvedPath ipath.Resolved, newContentPath ipath.Path, continueProcessing bool, hadMatchingRule bool) { - redirectsFile := i.getRedirectsFile(r, contentPath, logger) - if redirectsFile != nil { - redirectRules, err := i.getRedirectRules(r, redirectsFile) - if err != nil { - internalWebError(w, err) - return nil, nil, false, true - } - - redirected, newPath, err := i.handleRedirectsFileRules(w, r, contentPath, redirectRules) - if err != nil { - err = fmt.Errorf("trouble processing _redirects file at %q: %w", redirectsFile.String(), err) - internalWebError(w, err) - return nil, nil, false, true - } - - if redirected { - return nil, nil, false, true - } - - // 200 is treated as a rewrite, so update the path and continue - if newPath != "" { - // Reassign contentPath and resolvedPath since the URL was rewritten - contentPath = ipath.New(newPath) - resolvedPath, err = i.api.ResolvePath(r.Context(), contentPath) - if err != nil { - internalWebError(w, err) - return nil, nil, false, true - } - - return resolvedPath, contentPath, true, true - } - } - // No matching rule, paths remain the same, continue regular processing - return resolvedPath, contentPath, true, false -} - -func (i *handler) handleRedirectsFileRules(w http.ResponseWriter, r *http.Request, contentPath ipath.Path, redirectRules []redirects.Rule) (redirected bool, newContentPath string, err error) { - // Attempt to match a rule to the URL path, and perform the corresponding redirect or rewrite - pathParts := strings.Split(contentPath.String(), "/") - if len(pathParts) > 3 { - // All paths should start with /ipfs/cid/, so get the path after that - urlPath := "/" + strings.Join(pathParts[3:], "/") - rootPath := strings.Join(pathParts[:3], "/") - // Trim off the trailing / - urlPath = strings.TrimSuffix(urlPath, "/") - - for _, rule := range redirectRules { - // Error right away if the rule is invalid - if !rule.MatchAndExpandPlaceholders(urlPath) { - continue - } - - // We have a match! - - // Rewrite - if rule.Status == 200 { - // Prepend the rootPath - toPath := rootPath + rule.To - return false, toPath, nil - } - - // Or 4xx - if rule.Status == 404 || rule.Status == 410 || rule.Status == 451 { - toPath := rootPath + rule.To - content4xxPath := ipath.New(toPath) - err := i.serve4xx(w, r, content4xxPath, rule.Status) - return true, toPath, err - } - - // Or redirect - if rule.Status >= 301 && rule.Status <= 308 { - http.Redirect(w, r, rule.To, rule.Status) - return true, "", nil - } - } - } - - // No redirects matched - return false, "", nil -} - -func (i *handler) getRedirectRules(r *http.Request, redirectsFilePath ipath.Resolved) ([]redirects.Rule, error) { - // Convert the path into a file node - node, err := i.api.GetUnixFsNode(r.Context(), redirectsFilePath) - if err != nil { - return nil, fmt.Errorf("could not get _redirects: %w", err) - } - defer node.Close() - - // Convert the node into a file - f, ok := node.(files.File) - if !ok { - return nil, fmt.Errorf("could not parse _redirects: %w", err) - } - - // Parse redirect rules from file - redirectRules, err := redirects.Parse(f) - if err != nil { - return nil, fmt.Errorf("could not parse _redirects: %w", err) - } - - return redirectRules, nil -} - -// Returns a resolved path to the _redirects file located in the root CID path of the requested path -func (i *handler) getRedirectsFile(r *http.Request, contentPath ipath.Path, logger *zap.SugaredLogger) ipath.Resolved { - // contentPath is the full ipfs path to the requested resource, - // regardless of whether path or subdomain resolution is used. - rootPath := getRootPath(contentPath) - - // Check for _redirects file. - // Any path resolution failures are ignored and we just assume there's no _redirects file. - // Note that ignoring these errors also ensures that the use of the empty CID (bafkqaaa) in tests doesn't fail. - path := ipath.Join(rootPath, "_redirects") - resolvedPath, err := i.api.ResolvePath(r.Context(), path) - if err != nil { - return nil - } - return resolvedPath -} - -// Returns the root CID Path for the given path -func getRootPath(path ipath.Path) ipath.Path { - parts := strings.Split(path.String(), "/") - return ipath.New(gopath.Join("/", path.Namespace(), parts[2])) -} - -func (i *handler) serve4xx(w http.ResponseWriter, r *http.Request, content4xxPath ipath.Path, status int) error { - resolved4xxPath, err := i.api.ResolvePath(r.Context(), content4xxPath) - if err != nil { - return err - } - - node, err := i.api.GetUnixFsNode(r.Context(), resolved4xxPath) - if err != nil { - return err - } - defer node.Close() - - f, ok := node.(files.File) - if !ok { - return fmt.Errorf("could not convert node for %d page to file", status) - } - - size, err := f.Size() - if err != nil { - return fmt.Errorf("could not get size of %d page", status) - } - - log.Debugf("using _redirects: custom %d file at %q", status, content4xxPath) - w.Header().Set("Content-Type", "text/html") - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - addCacheControlHeaders(w, r, content4xxPath, resolved4xxPath.Cid()) - w.WriteHeader(status) - _, err = io.CopyN(w, f, size) - return err -} - -func hasOriginIsolation(r *http.Request) bool { - _, gw := r.Context().Value(GatewayHostnameKey).(string) - _, dnslink := r.Context().Value(DNSLinkHostnameKey).(string) - - if gw || dnslink { - return true - } - - return false -} - -func isUnixfsResponseFormat(responseFormat string) bool { - // The implicit response format is UnixFS - return responseFormat == "" -} - -// Deprecated: legacy ipfs-404.html files are superseded by _redirects file -// This is provided only for backward-compatibility, until websites migrate -// to 404s managed via _redirects file (https://github.com/ipfs/specs/pull/290) -func (i *handler) serveLegacy404IfPresent(w http.ResponseWriter, r *http.Request, contentPath ipath.Path) bool { - resolved404Path, ctype, err := i.searchUpTreeFor404(r, contentPath) - if err != nil { - return false - } - - dr, err := i.api.GetUnixFsNode(r.Context(), resolved404Path) - if err != nil { - return false - } - defer dr.Close() - - f, ok := dr.(files.File) - if !ok { - return false - } - - size, err := f.Size() - if err != nil { - return false - } - - log.Debugw("using pretty 404 file", "path", contentPath) - w.Header().Set("Content-Type", ctype) - w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) - w.WriteHeader(http.StatusNotFound) - _, err = io.CopyN(w, f, size) - return err == nil -} - -func (i *handler) searchUpTreeFor404(r *http.Request, contentPath ipath.Path) (ipath.Resolved, string, error) { - filename404, ctype, err := preferred404Filename(r.Header.Values("Accept")) - if err != nil { - return nil, "", err - } - - pathComponents := strings.Split(contentPath.String(), "/") - - for idx := len(pathComponents); idx >= 3; idx-- { - pretty404 := gopath.Join(append(pathComponents[0:idx], filename404)...) - parsed404Path := ipath.New("/" + pretty404) - if parsed404Path.IsValid() != nil { - break - } - resolvedPath, err := i.api.ResolvePath(r.Context(), parsed404Path) - if err != nil { - continue - } - return resolvedPath, ctype, nil - } - - return nil, "", fmt.Errorf("no pretty 404 in any parent folder") -} - -func preferred404Filename(acceptHeaders []string) (string, string, error) { - // If we ever want to offer a 404 file for a different content type - // then this function will need to parse q weightings, but for now - // the presence of anything matching HTML is enough. - for _, acceptHeader := range acceptHeaders { - accepted := strings.Split(acceptHeader, ",") - for _, spec := range accepted { - contentType := strings.SplitN(spec, ";", 1)[0] - switch contentType { - case "*/*", "text/*", "text/html": - return "ipfs-404.html", "text/html", nil - } - } - } - - return "", "", fmt.Errorf("there is no 404 file for the requested content types") -} diff --git a/extern/go-libipfs/gateway/handler_unixfs_dir.go b/extern/go-libipfs/gateway/handler_unixfs_dir.go deleted file mode 100644 index 005ee64f9..000000000 --- a/extern/go-libipfs/gateway/handler_unixfs_dir.go +++ /dev/null @@ -1,211 +0,0 @@ -package gateway - -import ( - "context" - "net/http" - "net/url" - gopath "path" - "strings" - "time" - - "github.com/dustin/go-humanize" - "github.com/filecoin-project/boost/extern/go-libipfs/gateway/assets" - cid "github.com/ipfs/go-cid" - "github.com/ipfs/go-ipfs-files" - path "github.com/ipfs/go-path" - "github.com/ipfs/go-path/resolver" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -// serveDirectory returns the best representation of UnixFS directory -// -// It will return index.html if present, or generate directory listing otherwise. -func (i *handler) serveDirectory(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, dir files.Directory, begin time.Time, logger *zap.SugaredLogger) bool { - ctx, span := spanTrace(ctx, "ServeDirectory", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - // HostnameOption might have constructed an IPNS/IPFS path using the Host header. - // In this case, we need the original path for constructing redirects - // and links that match the requested URL. - // For example, http://example.net would become /ipns/example.net, and - // the redirects and links would end up as http://example.net/ipns/example.net - requestURI, err := url.ParseRequestURI(r.RequestURI) - if err != nil { - webError(w, "failed to parse request path", err, http.StatusInternalServerError) - return false - } - originalURLPath := requestURI.Path - - // Ensure directory paths end with '/' - if originalURLPath[len(originalURLPath)-1] != '/' { - // don't redirect to trailing slash if it's go get - // https://github.com/ipfs/kubo/pull/3963 - goget := r.URL.Query().Get("go-get") == "1" - if !goget { - suffix := "/" - // preserve query parameters - if r.URL.RawQuery != "" { - suffix = suffix + "?" + r.URL.RawQuery - } - // /ipfs/cid/foo?bar must be redirected to /ipfs/cid/foo/?bar - redirectURL := originalURLPath + suffix - logger.Debugw("directory location moved permanently", "status", http.StatusMovedPermanently) - http.Redirect(w, r, redirectURL, http.StatusMovedPermanently) - return true - } - } - - // Check if directory has index.html, if so, serveFile - idxPath := ipath.Join(contentPath, "index.html") - idxResolvedPath, err := i.api.ResolvePath(ctx, idxPath) - switch err.(type) { - case nil: - idx, err := i.api.GetUnixFsNode(ctx, idxResolvedPath) - if err != nil { - internalWebError(w, err) - return false - } - - f, ok := idx.(files.File) - if !ok { - internalWebError(w, files.ErrNotReader) - return false - } - - logger.Debugw("serving index.html file", "path", idxPath) - // write to request - success := i.serveFile(ctx, w, r, resolvedPath, idxPath, f, begin) - if success { - i.unixfsDirIndexGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } - return success - case resolver.ErrNoLink: - logger.Debugw("no index.html; noop", "path", idxPath) - default: - internalWebError(w, err) - return false - } - - // See statusResponseWriter.WriteHeader - // and https://github.com/ipfs/kubo/issues/7164 - // Note: this needs to occur before listingTemplate.Execute otherwise we get - // superfluous response.WriteHeader call from prometheus/client_golang - if w.Header().Get("Location") != "" { - logger.Debugw("location moved permanently", "status", http.StatusMovedPermanently) - w.WriteHeader(http.StatusMovedPermanently) - return true - } - - // A HTML directory index will be presented, be sure to set the correct - // type instead of relying on autodetection (which may fail). - w.Header().Set("Content-Type", "text/html") - - // Generated dir index requires custom Etag (output may change between go-ipfs versions) - dirEtag := getDirListingEtag(resolvedPath.Cid()) - w.Header().Set("Etag", dirEtag) - - if r.Method == http.MethodHead { - logger.Debug("return as request's HTTP method is HEAD") - return true - } - - results, err := i.api.LsUnixFsDir(ctx, resolvedPath) - if err != nil { - internalWebError(w, err) - return false - } - - dirListing := make([]assets.DirectoryItem, 0, len(results)) - for link := range results { - if link.Err != nil { - internalWebError(w, link.Err) - return false - } - - hash := link.Cid.String() - di := assets.DirectoryItem{ - Size: humanize.Bytes(uint64(link.Size)), - Name: link.Name, - Path: gopath.Join(originalURLPath, link.Name), - Hash: hash, - ShortHash: assets.ShortHash(hash), - } - dirListing = append(dirListing, di) - } - - // construct the correct back link - // https://github.com/ipfs/kubo/issues/1365 - backLink := originalURLPath - - // don't go further up than /ipfs/$hash/ - pathSplit := path.SplitList(contentPath.String()) - switch { - // skip backlink when listing a content root - case len(pathSplit) == 3: // url: /ipfs/$hash - backLink = "" - - // skip backlink when listing a content root - case len(pathSplit) == 4 && pathSplit[3] == "": // url: /ipfs/$hash/ - backLink = "" - - // add the correct link depending on whether the path ends with a slash - default: - if strings.HasSuffix(backLink, "/") { - backLink += ".." - } else { - backLink += "/.." - } - } - - size := "?" - if s, err := dir.Size(); err == nil { - // Size may not be defined/supported. Continue anyways. - size = humanize.Bytes(uint64(s)) - } - - hash := resolvedPath.Cid().String() - - // Gateway root URL to be used when linking to other rootIDs. - // This will be blank unless subdomain or DNSLink resolution is being used - // for this request. - var gwURL string - - // Get gateway hostname and build gateway URL. - if h, ok := r.Context().Value(GatewayHostnameKey).(string); ok { - gwURL = "//" + h - } else { - gwURL = "" - } - - dnslink := assets.HasDNSLinkOrigin(gwURL, contentPath.String()) - - // See comment above where originalUrlPath is declared. - tplData := assets.DirectoryTemplateData{ - GatewayURL: gwURL, - DNSLink: dnslink, - Listing: dirListing, - Size: size, - Path: contentPath.String(), - Breadcrumbs: assets.Breadcrumbs(contentPath.String(), dnslink), - BackLink: backLink, - Hash: hash, - } - - logger.Debugw("request processed", "tplDataDNSLink", dnslink, "tplDataSize", size, "tplDataBackLink", backLink, "tplDataHash", hash) - - if err := assets.DirectoryTemplate.Execute(w, tplData); err != nil { - internalWebError(w, err) - return false - } - - // Update metrics - i.unixfsGenDirListingGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - return true -} - -func getDirListingEtag(dirCid cid.Cid) string { - return `"DirIndex-` + assets.AssetHash + `_CID-` + dirCid.String() + `"` -} diff --git a/extern/go-libipfs/gateway/handler_unixfs_file.go b/extern/go-libipfs/gateway/handler_unixfs_file.go deleted file mode 100644 index 55a61ee8c..000000000 --- a/extern/go-libipfs/gateway/handler_unixfs_file.go +++ /dev/null @@ -1,105 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "io" - "mime" - "net/http" - gopath "path" - "strings" - "time" - - "github.com/gabriel-vasile/mimetype" - "github.com/ipfs/go-libipfs/files" - ipath "github.com/ipfs/interface-go-ipfs-core/path" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" -) - -// serveFile returns data behind a file along with HTTP headers based on -// the file itself, its CID and the contentPath used for accessing it. -func (i *handler) serveFile(ctx context.Context, w http.ResponseWriter, r *http.Request, resolvedPath ipath.Resolved, contentPath ipath.Path, file files.File, begin time.Time) bool { - _, span := spanTrace(ctx, "ServeFile", trace.WithAttributes(attribute.String("path", resolvedPath.String()))) - defer span.End() - - // Set Cache-Control and read optional Last-Modified time - modtime := addCacheControlHeaders(w, r, contentPath, resolvedPath.Cid()) - - // Set Content-Disposition - name := addContentDispositionHeader(w, r, contentPath) - - // Prepare size value for Content-Length HTTP header (set inside of http.ServeContent) - size, err := file.Size() - if err != nil { - http.Error(w, "cannot serve files with unknown sizes", http.StatusBadGateway) - return false - } - - if size == 0 { - // We override null files to 200 to avoid issues with fragment caching reverse proxies. - // Also whatever you are asking for, it's cheaper to just give you the complete file (nothing). - // TODO: remove this if clause once https://github.com/golang/go/issues/54794 is fixed in two latest releases of go - w.Header().Set("Content-Type", "text/plain") - w.WriteHeader(http.StatusOK) - return true - } - - // Lazy seeker enables efficient range-requests and HTTP HEAD responses - content := &lazySeeker{ - size: size, - reader: file, - } - - // Calculate deterministic value for Content-Type HTTP header - // (we prefer to do it here, rather than using implicit sniffing in http.ServeContent) - var ctype string - if _, isSymlink := file.(*files.Symlink); isSymlink { - // We should be smarter about resolving symlinks but this is the - // "most correct" we can be without doing that. - ctype = "inode/symlink" - } else { - ctype = mime.TypeByExtension(gopath.Ext(name)) - if ctype == "" { - // uses https://github.com/gabriel-vasile/mimetype library to determine the content type. - // Fixes https://github.com/ipfs/kubo/issues/7252 - mimeType, err := mimetype.DetectReader(content) - if err != nil { - http.Error(w, fmt.Sprintf("cannot detect content-type: %s", err.Error()), http.StatusInternalServerError) - return false - } - - ctype = mimeType.String() - _, err = content.Seek(0, io.SeekStart) - if err != nil { - http.Error(w, "seeker can't seek", http.StatusInternalServerError) - return false - } - } - // Strip the encoding from the HTML Content-Type header and let the - // browser figure it out. - // - // Fixes https://github.com/ipfs/kubo/issues/2203 - if strings.HasPrefix(ctype, "text/html;") { - ctype = "text/html" - } - } - // Setting explicit Content-Type to avoid mime-type sniffing on the client - // (unifies behavior across gateways and web browsers) - w.Header().Set("Content-Type", ctype) - - // special fixup around redirects - w = &statusResponseWriter{w} - - // ServeContent will take care of - // If-None-Match+Etag, Content-Length and range requests - _, dataSent, _ := ServeContent(w, r, name, modtime, content) - - // Was response successful? - if dataSent { - // Update metrics - i.unixfsFileGetMetric.WithLabelValues(contentPath.Namespace()).Observe(time.Since(begin).Seconds()) - } - - return dataSent -} diff --git a/extern/go-libipfs/gateway/hostname.go b/extern/go-libipfs/gateway/hostname.go deleted file mode 100644 index 563804e12..000000000 --- a/extern/go-libipfs/gateway/hostname.go +++ /dev/null @@ -1,592 +0,0 @@ -package gateway - -import ( - "context" - "fmt" - "net" - "net/http" - "net/url" - "regexp" - "strings" - - cid "github.com/ipfs/go-cid" - "github.com/libp2p/go-libp2p/core/peer" - dns "github.com/miekg/dns" - - mbase "github.com/multiformats/go-multibase" -) - -// Specification is the specification of an IPFS Public Gateway. -type Specification struct { - // Paths is explicit list of path prefixes that should be handled by - // this gateway. Example: `["/ipfs", "/ipns"]` - // Useful if you only want to support immutable `/ipfs`. - Paths []string - - // UseSubdomains indicates whether or not this gateway uses subdomains - // for IPFS resources instead of paths. That is: http://CID.ipfs.GATEWAY/... - // - // If this flag is set, any /ipns/$id and/or /ipfs/$id paths in Paths - // will be permanently redirected to http://$id.[ipns|ipfs].$gateway/. - // - // We do not support using both paths and subdomains for a single domain - // for security reasons (Origin isolation). - UseSubdomains bool - - // NoDNSLink configures this gateway to _not_ resolve DNSLink for the - // specific FQDN provided in `Host` HTTP header. Useful when you want to - // explicitly allow or refuse hosting a single hostname. To refuse all - // DNSLinks in `Host` processing, pass noDNSLink to `WithHostname` instead. - // This flag overrides the global one. - NoDNSLink bool - - // InlineDNSLink configures this gateway to always inline DNSLink names - // (FQDN) into a single DNS label in order to interop with wildcard TLS certs - // and Origin per CID isolation provided by rules like https://publicsuffix.org - // This should be set to true if you use HTTPS. - InlineDNSLink bool -} - -// WithHostname is a middleware that can wrap an http.Handler in order to parse the -// Host header and translating it to the content path. This is useful for Subdomain -// and DNSLink gateways. -// -// publicGateways configures the behavior of known public gateways. Each key is a -// fully qualified domain name (FQDN). -// -// noDNSLink configures the gateway to _not_ perform DNS TXT record lookups in -// response to requests with values in `Host` HTTP header. This flag can be overridden -// per FQDN in publicGateways. -func WithHostname(next http.Handler, api API, publicGateways map[string]*Specification, noDNSLink bool) http.HandlerFunc { - gateways := prepareHostnameGateways(publicGateways) - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Unfortunately, many (well, ipfs.io) gateways use - // DNSLink so if we blindly rewrite with DNSLink, we'll - // break /ipfs links. - // - // We fix this by maintaining a list of known gateways - // and the paths that they serve "gateway" content on. - // That way, we can use DNSLink for everything else. - - // Support X-Forwarded-Host if added by a reverse proxy - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host - host := r.Host - if xHost := r.Header.Get("X-Forwarded-Host"); xHost != "" { - host = xHost - } - - // HTTP Host & Path check: is this one of our "known gateways"? - if gw, ok := gateways.isKnownHostname(host); ok { - // This is a known gateway but request is not using - // the subdomain feature. - - // Does this gateway _handle_ this path? - if hasPrefix(r.URL.Path, gw.Paths...) { - // It does. - - // Should this gateway use subdomains instead of paths? - if gw.UseSubdomains { - // Yes, redirect if applicable - // Example: dweb.link/ipfs/{cid} → {cid}.ipfs.dweb.link - useInlinedDNSLink := gw.InlineDNSLink - newURL, err := toSubdomainURL(host, r.URL.Path, r, useInlinedDNSLink, api) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if newURL != "" { - // Set "Location" header with redirect destination. - // It is ignored by curl in default mode, but will - // be respected by user agents that follow - // redirects by default, namely web browsers - w.Header().Set("Location", newURL) - - // Note: we continue regular gateway processing: - // HTTP Status Code http.StatusMovedPermanently - // will be set later, in statusResponseWriter - } - } - - // Not a subdomain resource, continue with path processing - // Example: 127.0.0.1:8080/ipfs/{CID}, ipfs.io/ipfs/{CID} etc - next.ServeHTTP(w, r) - return - } - // Not a whitelisted path - - // Try DNSLink, if it was not explicitly disabled for the hostname - if !gw.NoDNSLink && hasDNSLinkRecord(r.Context(), api, host) { - // rewrite path and handle as DNSLink - r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path - next.ServeHTTP(w, withHostnameContext(r, host)) - return - } - - // If not, resource does not exist on the hostname, return 404 - http.NotFound(w, r) - return - } - - // HTTP Host check: is this one of our subdomain-based "known gateways"? - // IPFS details extracted from the host: {rootID}.{ns}.{gwHostname} - // /ipfs/ example: {cid}.ipfs.localhost:8080, {cid}.ipfs.dweb.link - // /ipns/ example: {libp2p-key}.ipns.localhost:8080, {inlined-dnslink-fqdn}.ipns.dweb.link - if gw, gwHostname, ns, rootID, ok := gateways.knownSubdomainDetails(host); ok { - // Looks like we're using a known gateway in subdomain mode. - - // Assemble original path prefix. - pathPrefix := "/" + ns + "/" + rootID - - // Retrieve whether or not we should inline DNSLink. - useInlinedDNSLink := gw.InlineDNSLink - - // Does this gateway _handle_ subdomains AND this path? - if !(gw.UseSubdomains && hasPrefix(pathPrefix, gw.Paths...)) { - // If not, resource does not exist, return 404 - http.NotFound(w, r) - return - } - - // Check if rootID is a valid CID - if rootCID, err := cid.Decode(rootID); err == nil { - // Do we need to redirect root CID to a canonical DNS representation? - dnsCID, err := toDNSLabel(rootID, rootCID) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if !strings.HasPrefix(r.Host, dnsCID) { - dnsPrefix := "/" + ns + "/" + dnsCID - newURL, err := toSubdomainURL(gwHostname, dnsPrefix+r.URL.Path, r, useInlinedDNSLink, api) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if newURL != "" { - // Redirect to deterministic CID to ensure CID - // always gets the same Origin on the web - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - return - } - } - - // Do we need to fix multicodec in PeerID represented as CIDv1? - if isPeerIDNamespace(ns) { - if rootCID.Type() != cid.Libp2pKey { - newURL, err := toSubdomainURL(gwHostname, pathPrefix+r.URL.Path, r, useInlinedDNSLink, api) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - if newURL != "" { - // Redirect to CID fixed inside of toSubdomainURL() - http.Redirect(w, r, newURL, http.StatusMovedPermanently) - return - } - } - } - } else { // rootID is not a CID.. - // Check if rootID is a single DNS label with an inlined - // DNSLink FQDN a single DNS label. We support this so - // loading DNSLink names over TLS "just works" on public - // HTTP gateways. - // - // Rationale for doing this can be found under "Option C" - // at: https://github.com/ipfs/in-web-browsers/issues/169 - // - // TLDR is: - // https://dweb.link/ipns/my.v-long.example.com - // can be loaded from a subdomain gateway with a wildcard - // TLS cert if represented as a single DNS label: - // https://my-v--long-example-com.ipns.dweb.link - if ns == "ipns" && !strings.Contains(rootID, ".") { - // if there is no TXT recordfor rootID - if !hasDNSLinkRecord(r.Context(), api, rootID) { - // my-v--long-example-com → my.v-long.example.com - dnslinkFQDN := toDNSLinkFQDN(rootID) - if hasDNSLinkRecord(r.Context(), api, dnslinkFQDN) { - // update path prefix to use real FQDN with DNSLink - pathPrefix = "/ipns/" + dnslinkFQDN - } - } - } - } - - // Rewrite the path to not use subdomains - r.URL.Path = pathPrefix + r.URL.Path - - // Serve path request - next.ServeHTTP(w, withHostnameContext(r, gwHostname)) - return - } - - // We don't have a known gateway. Fallback on DNSLink lookup - - // Wildcard HTTP Host check: - // 1. is wildcard DNSLink enabled (Gateway.NoDNSLink=false)? - // 2. does Host header include a fully qualified domain name (FQDN)? - // 3. does DNSLink record exist in DNS? - if !noDNSLink && hasDNSLinkRecord(r.Context(), api, host) { - // rewrite path and handle as DNSLink - r.URL.Path = "/ipns/" + stripPort(host) + r.URL.Path - ctx := context.WithValue(r.Context(), DNSLinkHostnameKey, host) - next.ServeHTTP(w, withHostnameContext(r.WithContext(ctx), host)) - return - } - - // else, treat it as an old school gateway, I guess. - next.ServeHTTP(w, r) - - }) -} - -// Extends request context to include hostname of a canonical gateway root -// (subdomain root or dnslink fqdn) -func withHostnameContext(r *http.Request, hostname string) *http.Request { - // This is required for links on directory listing pages to work correctly - // on subdomain and dnslink gateways. While DNSlink could read value from - // Host header, subdomain gateways have more comples rules (knownSubdomainDetails) - // More: https://github.com/ipfs/dir-index-html/issues/42 - // nolint: staticcheck // non-backward compatible change - ctx := context.WithValue(r.Context(), GatewayHostnameKey, hostname) - return r.WithContext(ctx) -} - -// isDomainNameAndNotPeerID returns bool if string looks like a valid DNS name AND is not a PeerID -func isDomainNameAndNotPeerID(hostname string) bool { - if len(hostname) == 0 { - return false - } - if _, err := peer.Decode(hostname); err == nil { - return false - } - _, ok := dns.IsDomainName(hostname) - return ok -} - -// hasDNSLinkRecord returns if a DNS TXT record exists for the provided host. -func hasDNSLinkRecord(ctx context.Context, api API, host string) bool { - dnslinkName := stripPort(host) - - if !isDomainNameAndNotPeerID(dnslinkName) { - return false - } - - _, err := api.GetDNSLinkRecord(ctx, dnslinkName) - return err == nil -} - -func isSubdomainNamespace(ns string) bool { - switch ns { - case "ipfs", "ipns", "p2p", "ipld": - // Note: 'p2p' and 'ipld' is only kept here for compatibility with Kubo. - return true - default: - return false - } -} - -func isPeerIDNamespace(ns string) bool { - switch ns { - case "ipns", "p2p": - // Note: 'p2p' and 'ipld' is only kept here for compatibility with Kubo. - return true - default: - return false - } -} - -// Label's max length in DNS (https://tools.ietf.org/html/rfc1034#page-7) -const dnsLabelMaxLength int = 63 - -// Converts a CID to DNS-safe representation that fits in 63 characters -func toDNSLabel(rootID string, rootCID cid.Cid) (dnsCID string, err error) { - // Return as-is if things fit - if len(rootID) <= dnsLabelMaxLength { - return rootID, nil - } - - // Convert to Base36 and see if that helped - rootID, err = cid.NewCidV1(rootCID.Type(), rootCID.Hash()).StringOfBase(mbase.Base36) - if err != nil { - return "", err - } - if len(rootID) <= dnsLabelMaxLength { - return rootID, nil - } - - // Can't win with DNS at this point, return error - return "", fmt.Errorf("CID incompatible with DNS label length limit of 63: %s", rootID) -} - -// Returns true if HTTP request involves TLS certificate. -// See https://github.com/ipfs/in-web-browsers/issues/169 to understand how it -// impacts DNSLink websites on public gateways. -func isHTTPSRequest(r *http.Request) bool { - // X-Forwarded-Proto if added by a reverse proxy - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto - xproto := r.Header.Get("X-Forwarded-Proto") - // Is request a native TLS (not used atm, but future-proofing) - // or a proxied HTTPS (eg. go-ipfs behind nginx at a public gw)? - return r.URL.Scheme == "https" || xproto == "https" -} - -// Converts a FQDN to DNS-safe representation that fits in 63 characters: -// my.v-long.example.com → my-v--long-example-com -func toDNSLinkDNSLabel(fqdn string) (dnsLabel string, err error) { - dnsLabel = strings.ReplaceAll(fqdn, "-", "--") - dnsLabel = strings.ReplaceAll(dnsLabel, ".", "-") - if len(dnsLabel) > dnsLabelMaxLength { - return "", fmt.Errorf("DNSLink representation incompatible with DNS label length limit of 63: %s", dnsLabel) - } - return dnsLabel, nil -} - -// Converts a DNS-safe representation of DNSLink FQDN to real FQDN: -// my-v--long-example-com → my.v-long.example.com -func toDNSLinkFQDN(dnsLabel string) (fqdn string) { - fqdn = strings.ReplaceAll(dnsLabel, "--", "@") // @ placeholder is unused in DNS labels - fqdn = strings.ReplaceAll(fqdn, "-", ".") - fqdn = strings.ReplaceAll(fqdn, "@", "-") - return fqdn -} - -// Converts a hostname/path to a subdomain-based URL, if applicable. -func toSubdomainURL(hostname, path string, r *http.Request, inlineDNSLink bool, api API) (redirURL string, err error) { - var scheme, ns, rootID, rest string - - query := r.URL.RawQuery - parts := strings.SplitN(path, "/", 4) - isHTTPS := isHTTPSRequest(r) - safeRedirectURL := func(in string) (out string, err error) { - safeURI, err := url.ParseRequestURI(in) - if err != nil { - return "", err - } - return safeURI.String(), nil - } - - if isHTTPS { - scheme = "https:" - } else { - scheme = "http:" - } - - switch len(parts) { - case 4: - rest = parts[3] - fallthrough - case 3: - ns = parts[1] - rootID = parts[2] - default: - return "", nil - } - - if !isSubdomainNamespace(ns) { - return "", nil - } - - // add prefix if query is present - if query != "" { - query = "?" + query - } - - // Normalize problematic PeerIDs (eg. ed25519+identity) to CID representation - if isPeerIDNamespace(ns) && !isDomainNameAndNotPeerID(rootID) { - peerID, err := peer.Decode(rootID) - // Note: PeerID CIDv1 with protobuf multicodec will fail, but we fix it - // in the next block - if err == nil { - rootID = peer.ToCid(peerID).String() - } - } - - // If rootID is a CID, ensure it uses DNS-friendly text representation - if rootCID, err := cid.Decode(rootID); err == nil { - multicodec := rootCID.Type() - var base mbase.Encoding = mbase.Base32 - - // Normalizations specific to /ipns/{libp2p-key} - if isPeerIDNamespace(ns) { - // Using Base36 for /ipns/ for consistency - // Context: https://github.com/ipfs/kubo/pull/7441#discussion_r452372828 - base = mbase.Base36 - - // PeerIDs represented as CIDv1 are expected to have libp2p-key - // multicodec (https://github.com/libp2p/specs/pull/209). - // We ease the transition by fixing multicodec on the fly: - // https://github.com/ipfs/kubo/issues/5287#issuecomment-492163929 - if multicodec != cid.Libp2pKey { - multicodec = cid.Libp2pKey - } - } - - // Ensure CID text representation used in subdomain is compatible - // with the way DNS and URIs are implemented in user agents. - // - // 1. Switch to CIDv1 and enable case-insensitive Base encoding - // to avoid issues when user agent force-lowercases the hostname - // before making the request - // (https://github.com/ipfs/in-web-browsers/issues/89) - rootCID = cid.NewCidV1(multicodec, rootCID.Hash()) - rootID, err = rootCID.StringOfBase(base) - if err != nil { - return "", err - } - // 2. Make sure CID fits in a DNS label, adjust encoding if needed - // (https://github.com/ipfs/kubo/issues/7318) - rootID, err = toDNSLabel(rootID, rootCID) - if err != nil { - return "", err - } - } else { // rootID is not a CID - - // Check if rootID is a FQDN with DNSLink and convert it to TLS-safe - // representation that fits in a single DNS label. We support this so - // loading DNSLink names over TLS "just works" on public HTTP gateways - // that pass 'https' in X-Forwarded-Proto to go-ipfs. - // - // Rationale can be found under "Option C" - // at: https://github.com/ipfs/in-web-browsers/issues/169 - // - // TLDR is: - // /ipns/my.v-long.example.com - // can be loaded from a subdomain gateway with a wildcard TLS cert if - // represented as a single DNS label: - // https://my-v--long-example-com.ipns.dweb.link - if (inlineDNSLink || isHTTPS) && ns == "ipns" && strings.Contains(rootID, ".") { - if hasDNSLinkRecord(r.Context(), api, rootID) { - // my.v-long.example.com → my-v--long-example-com - dnsLabel, err := toDNSLinkDNSLabel(rootID) - if err != nil { - return "", err - } - // update path prefix to use real FQDN with DNSLink - rootID = dnsLabel - } - } - } - - return safeRedirectURL(fmt.Sprintf( - "%s//%s.%s.%s/%s%s", - scheme, - rootID, - ns, - hostname, - rest, - query, - )) -} - -func hasPrefix(path string, prefixes ...string) bool { - for _, prefix := range prefixes { - // Assume people are creative with trailing slashes in Gateway config - p := strings.TrimSuffix(prefix, "/") - // Support for both /version and /ipfs/$cid - if p == path || strings.HasPrefix(path, p+"/") { - return true - } - } - return false -} - -func stripPort(hostname string) string { - host, _, err := net.SplitHostPort(hostname) - if err == nil { - return host - } - return hostname -} - -type hostnameGateways struct { - exact map[string]*Specification - wildcard map[*regexp.Regexp]*Specification -} - -// prepareHostnameGateways converts the user given gateways into an internal format -// split between exact and wildcard-based gateway hostnames. -func prepareHostnameGateways(gateways map[string]*Specification) *hostnameGateways { - h := &hostnameGateways{ - exact: map[string]*Specification{}, - wildcard: map[*regexp.Regexp]*Specification{}, - } - - for hostname, gw := range gateways { - if strings.Contains(hostname, "*") { - // from *.domain.tld, construct a regexp that match any direct subdomain - // of .domain.tld. - // - // Regexp will be in the form of ^[^.]+\.domain.tld(?::\d+)?$ - escaped := strings.ReplaceAll(hostname, ".", `\.`) - regexed := strings.ReplaceAll(escaped, "*", "[^.]+") - - re, err := regexp.Compile(fmt.Sprintf(`^%s(?::\d+)?$`, regexed)) - if err != nil { - log.Warn("invalid wildcard gateway hostname \"%s\"", hostname) - } - - h.wildcard[re] = gw - } else { - h.exact[hostname] = gw - } - } - - return h -} - -// isKnownHostname checks the given hostname gateways and returns a matching -// specification with graceful fallback to version without port. -func (gws *hostnameGateways) isKnownHostname(hostname string) (gw *Specification, ok bool) { - // Try hostname (host+optional port - value from Host header as-is) - if gw, ok := gws.exact[hostname]; ok { - return gw, ok - } - // Also test without port - if gw, ok = gws.exact[stripPort(hostname)]; ok { - return gw, ok - } - - // Wildcard support. Test both with and without port. - for re, spec := range gws.wildcard { - if re.MatchString(hostname) { - return spec, true - } - } - - return nil, false -} - -// knownSubdomainDetails parses the Host header and looks for a known gateway matching -// the subdomain host. If found, returns a Specification and the subdomain components -// extracted from Host header: {rootID}.{ns}.{gwHostname}. -// Note: hostname is host + optional port -func (gws *hostnameGateways) knownSubdomainDetails(hostname string) (gw *Specification, gwHostname, ns, rootID string, ok bool) { - labels := strings.Split(hostname, ".") - // Look for FQDN of a known gateway hostname. - // Example: given "dist.ipfs.tech.ipns.dweb.link": - // 1. Lookup "link" TLD in knownGateways: negative - // 2. Lookup "dweb.link" in knownGateways: positive - // - // Stops when we have 2 or fewer labels left as we need at least a - // rootId and a namespace. - for i := len(labels) - 1; i >= 2; i-- { - fqdn := strings.Join(labels[i:], ".") - gw, ok := gws.isKnownHostname(fqdn) - if !ok { - continue - } - - ns := labels[i-1] - if !isSubdomainNamespace(ns) { - continue - } - - // Merge remaining labels (could be a FQDN with DNSLink) - rootID := strings.Join(labels[:i-1], ".") - return gw, fqdn, ns, rootID, true - } - // no match - return nil, "", "", "", false -} diff --git a/extern/go-libipfs/gateway/lazyseek.go b/extern/go-libipfs/gateway/lazyseek.go deleted file mode 100644 index 0f4920fad..000000000 --- a/extern/go-libipfs/gateway/lazyseek.go +++ /dev/null @@ -1,60 +0,0 @@ -package gateway - -import ( - "fmt" - "io" -) - -// The HTTP server uses seek to determine the file size. Actually _seeking_ can -// be slow so we wrap the seeker in a _lazy_ seeker. -type lazySeeker struct { - reader io.ReadSeeker - - size int64 - offset int64 - realOffset int64 -} - -func (s *lazySeeker) Seek(offset int64, whence int) (int64, error) { - switch whence { - case io.SeekEnd: - return s.Seek(s.size+offset, io.SeekStart) - case io.SeekCurrent: - return s.Seek(s.offset+offset, io.SeekStart) - case io.SeekStart: - if offset < 0 { - return s.offset, fmt.Errorf("invalid seek offset") - } - s.offset = offset - return s.offset, nil - default: - return s.offset, fmt.Errorf("invalid whence: %d", whence) - } -} - -func (s *lazySeeker) Read(b []byte) (int, error) { - // If we're past the end, EOF. - if s.offset >= s.size { - return 0, io.EOF - } - - // actually seek - for s.offset != s.realOffset { - off, err := s.reader.Seek(s.offset, io.SeekStart) - if err != nil { - return 0, err - } - s.realOffset = off - } - off, err := s.reader.Read(b) - s.realOffset += int64(off) - s.offset += int64(off) - return off, err -} - -func (s *lazySeeker) Close() error { - if closer, ok := s.reader.(io.Closer); ok { - return closer.Close() - } - return nil -} diff --git a/extern/go-libipfs/gateway/testdata/fixtures.car b/extern/go-libipfs/gateway/testdata/fixtures.car deleted file mode 100644 index e01ca5c31c26f4c3769396f83455ac80a7037a4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1688 zcmcColv{nTFhK5L8N?7X0IF7%|s z5h1>i)Z!BN#FEtV#7g(n5(z6I7l_VAgXNw3PS!tlk?UE;vY74HR+%tWT>-5H1}~M% zruTo95RwGx^bGI|_Q)?T$xF;lbxKUm&dJQnE|%zL5^{!^lB?(SISzJFK*AA z7cq_R&}DVeX8*sTFSgxhpJFB?fo@7rYD#8NYI2FhEJm0ogjzg%W*q8%wPV8``7@$_ z=f$j!pZUHvMD@usKMgLPFC0R=AVUI*QcFrIO$?Og}ZSJ9Upyt_IIAcJeP+*#WH1mmR|TL*=OuIy+^m(_>&N8T3&upiUcdjWKCjp zfYncA1UZ6Wn1fOR&=J{fn~MLN{CPMxyTR{&lzq7S+9j_R>C1W^kPEuIy*=7nh$AgC zCsnVcqC|pG$QTk6;m^YDw{kp^?0PRD7=PyMvc5wB>yEBI)Fijq{}@-j#~h%Z{Cs-_ zWgB&gh2Zd0CB`PhZm`1%8ma-t^gtV*HRELW9_7z* zs!N2JlM;(0EWs%r=+7&~aaL+^V(%WA`msB-Chfc3op6rz;Jx#Y*NSadGMHBTKt6@L&RuiM{B!7rgTsT6CS$NF zLhd$d0f!pF%%Kg5?#COBIL-JYBQopvmlHGdcdDqbb9|CAd$P}xsWYW3f_8)oF~&bGQK{{j{mBx@0odU%ol-!wWX+PC1zm zc6rl3=7^tG`)?@jk|`s^mXexUkXj_+BV-P7(VyDLj5}*S-EN)e>A`CDH2d!6^qk_r zH_e9=A`?VcdrJuMW)`Fs>jH}nh@-ebj!FdinV?-z8|HAHoteKX=F$rBiK}Owy{dD~ ze$E7`tu^NE;v6g4yrQ(wZQuvlU<}G!gmk(9{i2XuT3nK!s{nE!NDm?ZIK!0#0LYJ= A8UO$Q diff --git a/go.mod b/go.mod index 5bdde2fe3..829e678cd 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect github.com/ipfs/go-ipfs-exchange-interface v0.2.0 github.com/ipfs/go-ipfs-exchange-offline v0.3.0 - github.com/ipfs/go-ipfs-files v0.3.0 + github.com/ipfs/go-ipfs-files v0.3.0 // indirect github.com/ipfs/go-ipfs-routing v0.3.0 github.com/ipfs/go-ipld-format v0.4.0 github.com/ipfs/go-ipld-legacy v0.1.1 @@ -135,13 +135,13 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bep/debounce v1.2.1 // indirect github.com/boltdb/bolt v1.3.1 // indirect - github.com/cespare/xxhash v1.1.0 + github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cilium/ebpf v0.7.0 // indirect github.com/containerd/cgroups v1.0.4 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 + github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 // indirect github.com/cskr/pubsub v1.0.2 // indirect github.com/daaku/go.zipexe v1.0.2 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect @@ -193,7 +193,7 @@ require ( github.com/go-openapi/swag v0.19.11 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect - github.com/gogo/protobuf v1.3.2 + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v1.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.2 // indirect @@ -225,10 +225,10 @@ require ( github.com/ipfs/go-ipfs-http-client v0.5.0 // indirect github.com/ipfs/go-ipfs-posinfo v0.0.1 // indirect github.com/ipfs/go-ipfs-pq v0.0.3 // indirect - github.com/ipfs/go-ipfs-util v0.0.2 + github.com/ipfs/go-ipfs-util v0.0.2 // indirect github.com/ipfs/go-ipld-cbor v0.0.6 - github.com/ipfs/go-ipns v0.3.0 - github.com/ipfs/go-log v1.0.5 + github.com/ipfs/go-ipns v0.3.0 // indirect + github.com/ipfs/go-log v1.0.5 // indirect github.com/ipfs/go-path v0.3.0 github.com/ipfs/go-peertaskqueue v0.8.1 // indirect github.com/ipfs/go-unixfsnode v1.5.2 @@ -271,7 +271,7 @@ require ( github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/miekg/dns v1.1.50 + github.com/miekg/dns v1.1.50 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect @@ -320,11 +320,11 @@ require ( github.com/zondax/hid v0.9.1 // indirect github.com/zondax/ledger-go v0.12.1 // indirect go.uber.org/dig v1.15.0 // indirect - go.uber.org/zap v1.24.0 + go.uber.org/zap v1.24.0 // indirect go4.org v0.0.0-20200411211856-f5505b9728dd // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.7.0 // indirect - golang.org/x/sys v0.5.0 + golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6 // indirect @@ -338,10 +338,10 @@ require ( require ( github.com/filecoin-project/boostd-data v0.0.0-00010101000000-000000000000 - github.com/gabriel-vasile/mimetype v1.4.1 + github.com/gabriel-vasile/mimetype v1.4.1 // indirect github.com/ipfs/go-ds-flatfs v0.5.1 github.com/ipfs/go-fetcher v1.6.1 - github.com/ipfs/go-ipfs-redirects-file v0.1.1 + github.com/ipfs/go-ipfs-redirects-file v0.1.1 // indirect github.com/ipfs/go-namesys v0.7.0 )