Write data about pushed images if env var is set
This commit is contained in:
parent
4b7e2b3a06
commit
295dd49487
|
|
@ -754,6 +754,17 @@
|
||||||
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
revision = "3e01752db0189b9157070a0e1668a620f9a85da2"
|
||||||
version = "v1.0.6"
|
version = "v1.0.6"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:90cf76d709ce9b057e7d75bd245bf7c1242d21ba4f908fb22c7a2a96d1dcc0ca"
|
||||||
|
name = "github.com/spf13/afero"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"mem",
|
||||||
|
]
|
||||||
|
pruneopts = "NUT"
|
||||||
|
revision = "f4711e4db9e9a1d3887343acb72b2bbfc2f686f5"
|
||||||
|
version = "v1.2.1"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:343d44e06621142ab09ae0c76c1799104cdfddd3ffb445d78b1adf8dc3ffaf3d"
|
digest = "1:343d44e06621142ab09ae0c76c1799104cdfddd3ffb445d78b1adf8dc3ffaf3d"
|
||||||
name = "github.com/spf13/cobra"
|
name = "github.com/spf13/cobra"
|
||||||
|
|
@ -1196,6 +1207,7 @@
|
||||||
"github.com/google/go-containerregistry/pkg/v1/empty",
|
"github.com/google/go-containerregistry/pkg/v1/empty",
|
||||||
"github.com/google/go-containerregistry/pkg/v1/mutate",
|
"github.com/google/go-containerregistry/pkg/v1/mutate",
|
||||||
"github.com/google/go-containerregistry/pkg/v1/partial",
|
"github.com/google/go-containerregistry/pkg/v1/partial",
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/random",
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote",
|
"github.com/google/go-containerregistry/pkg/v1/remote",
|
||||||
"github.com/google/go-containerregistry/pkg/v1/tarball",
|
"github.com/google/go-containerregistry/pkg/v1/tarball",
|
||||||
"github.com/google/go-github/github",
|
"github.com/google/go-github/github",
|
||||||
|
|
@ -1205,6 +1217,7 @@
|
||||||
"github.com/moby/buildkit/frontend/dockerfile/shell",
|
"github.com/moby/buildkit/frontend/dockerfile/shell",
|
||||||
"github.com/pkg/errors",
|
"github.com/pkg/errors",
|
||||||
"github.com/sirupsen/logrus",
|
"github.com/sirupsen/logrus",
|
||||||
|
"github.com/spf13/afero",
|
||||||
"github.com/spf13/cobra",
|
"github.com/spf13/cobra",
|
||||||
"github.com/spf13/pflag",
|
"github.com/spf13/pflag",
|
||||||
"golang.org/x/net/context",
|
"golang.org/x/net/context",
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,11 @@ package executor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/cache"
|
"github.com/GoogleContainerTools/kaniko/pkg/cache"
|
||||||
|
|
@ -29,13 +32,14 @@ import (
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/timing"
|
"github.com/GoogleContainerTools/kaniko/pkg/timing"
|
||||||
"github.com/GoogleContainerTools/kaniko/pkg/version"
|
"github.com/GoogleContainerTools/kaniko/pkg/version"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
"github.com/google/go-containerregistry/pkg/v1/tarball"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
type withUserAgent struct {
|
type withUserAgent struct {
|
||||||
|
|
@ -102,6 +106,39 @@ func DoPush(image v1.Image, opts *config.KanikoOptions) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timing.DefaultRun.Stop(t)
|
timing.DefaultRun.Stop(t)
|
||||||
|
return writeImageOutputs(image, destRefs)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fs = afero.NewOsFs()
|
||||||
|
|
||||||
|
func writeImageOutputs(image v1.Image, destRefs []name.Tag) error {
|
||||||
|
dir := os.Getenv("BUILDER_OUTPUT")
|
||||||
|
if dir == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
f, err := fs.Create(filepath.Join(dir, "images"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
d, err := image.Digest()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageOutput struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Digest string `json:"digest"`
|
||||||
|
}
|
||||||
|
for _, r := range destRefs {
|
||||||
|
if err := json.NewEncoder(f).Encode(imageOutput{
|
||||||
|
Name: r.String(),
|
||||||
|
Digest: d.String(),
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/random"
|
||||||
|
"github.com/spf13/afero"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mustTag(t *testing.T, s string) name.Tag {
|
||||||
|
tag, err := name.NewTag(s, name.StrictValidation)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewTag: %v", err)
|
||||||
|
}
|
||||||
|
return tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteImageOutputs(t *testing.T) {
|
||||||
|
img, err := random.Image(1024, 3)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("random.Image: %v", err)
|
||||||
|
}
|
||||||
|
d, err := img.Digest()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Digest: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range []struct {
|
||||||
|
desc, env string
|
||||||
|
tags []name.Tag
|
||||||
|
want string
|
||||||
|
}{{
|
||||||
|
desc: "env unset, no output",
|
||||||
|
env: "",
|
||||||
|
}, {
|
||||||
|
desc: "env set, one tag",
|
||||||
|
env: "/foo",
|
||||||
|
tags: []name.Tag{mustTag(t, "gcr.io/foo/bar:latest")},
|
||||||
|
want: fmt.Sprintf(`{"name":"gcr.io/foo/bar:latest","digest":%q}
|
||||||
|
`, d),
|
||||||
|
}, {
|
||||||
|
desc: "env set, two tags",
|
||||||
|
env: "/foo",
|
||||||
|
tags: []name.Tag{
|
||||||
|
mustTag(t, "gcr.io/foo/bar:latest"),
|
||||||
|
mustTag(t, "gcr.io/baz/qux:latest"),
|
||||||
|
},
|
||||||
|
want: fmt.Sprintf(`{"name":"gcr.io/foo/bar:latest","digest":%q}
|
||||||
|
{"name":"gcr.io/baz/qux:latest","digest":%q}
|
||||||
|
`, d, d),
|
||||||
|
}} {
|
||||||
|
t.Run(c.desc, func(t *testing.T) {
|
||||||
|
fs = afero.NewMemMapFs()
|
||||||
|
if c.want == "" {
|
||||||
|
fs = afero.NewReadOnlyFs(fs) // No files should be written.
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("BUILDER_OUTPUT", c.env)
|
||||||
|
if err := writeImageOutputs(img, c.tags); err != nil {
|
||||||
|
t.Fatalf("writeImageOutputs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.want == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := afero.ReadFile(fs, filepath.Join(c.env, "images"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := string(b); got != c.want {
|
||||||
|
t.Fatalf(" got: %s\nwant: %s", got, c.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
||||||
|
// Copyright 2013 tsuru authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// Package afero provides types and methods for interacting with the filesystem,
|
||||||
|
// as an abstraction layer.
|
||||||
|
|
||||||
|
// Afero also provides a few implementations that are mostly interoperable. One that
|
||||||
|
// uses the operating system filesystem, one that uses memory to store files
|
||||||
|
// (cross platform) and an interface that should be implemented if you want to
|
||||||
|
// provide your own filesystem.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Afero struct {
|
||||||
|
Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
// File represents a file in the filesystem.
|
||||||
|
type File interface {
|
||||||
|
io.Closer
|
||||||
|
io.Reader
|
||||||
|
io.ReaderAt
|
||||||
|
io.Seeker
|
||||||
|
io.Writer
|
||||||
|
io.WriterAt
|
||||||
|
|
||||||
|
Name() string
|
||||||
|
Readdir(count int) ([]os.FileInfo, error)
|
||||||
|
Readdirnames(n int) ([]string, error)
|
||||||
|
Stat() (os.FileInfo, error)
|
||||||
|
Sync() error
|
||||||
|
Truncate(size int64) error
|
||||||
|
WriteString(s string) (ret int, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fs is the filesystem interface.
|
||||||
|
//
|
||||||
|
// Any simulated or real filesystem should implement this interface.
|
||||||
|
type Fs interface {
|
||||||
|
// Create creates a file in the filesystem, returning the file and an
|
||||||
|
// error, if any happens.
|
||||||
|
Create(name string) (File, error)
|
||||||
|
|
||||||
|
// Mkdir creates a directory in the filesystem, return an error if any
|
||||||
|
// happens.
|
||||||
|
Mkdir(name string, perm os.FileMode) error
|
||||||
|
|
||||||
|
// MkdirAll creates a directory path and all parents that does not exist
|
||||||
|
// yet.
|
||||||
|
MkdirAll(path string, perm os.FileMode) error
|
||||||
|
|
||||||
|
// Open opens a file, returning it or an error, if any happens.
|
||||||
|
Open(name string) (File, error)
|
||||||
|
|
||||||
|
// OpenFile opens a file using the given flags and the given mode.
|
||||||
|
OpenFile(name string, flag int, perm os.FileMode) (File, error)
|
||||||
|
|
||||||
|
// Remove removes a file identified by name, returning an error, if any
|
||||||
|
// happens.
|
||||||
|
Remove(name string) error
|
||||||
|
|
||||||
|
// RemoveAll removes a directory path and any children it contains. It
|
||||||
|
// does not fail if the path does not exist (return nil).
|
||||||
|
RemoveAll(path string) error
|
||||||
|
|
||||||
|
// Rename renames a file.
|
||||||
|
Rename(oldname, newname string) error
|
||||||
|
|
||||||
|
// Stat returns a FileInfo describing the named file, or an error, if any
|
||||||
|
// happens.
|
||||||
|
Stat(name string) (os.FileInfo, error)
|
||||||
|
|
||||||
|
// The name of this FileSystem
|
||||||
|
Name() string
|
||||||
|
|
||||||
|
//Chmod changes the mode of the named file to mode.
|
||||||
|
Chmod(name string, mode os.FileMode) error
|
||||||
|
|
||||||
|
//Chtimes changes the access and modification times of the named file
|
||||||
|
Chtimes(name string, atime time.Time, mtime time.Time) error
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFileClosed = errors.New("File is closed")
|
||||||
|
ErrOutOfRange = errors.New("Out of range")
|
||||||
|
ErrTooLarge = errors.New("Too large")
|
||||||
|
ErrFileNotFound = os.ErrNotExist
|
||||||
|
ErrFileExists = os.ErrExist
|
||||||
|
ErrDestinationExists = os.ErrExist
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Lstater = (*BasePathFs)(nil)
|
||||||
|
|
||||||
|
// The BasePathFs restricts all operations to a given path within an Fs.
|
||||||
|
// The given file name to the operations on this Fs will be prepended with
|
||||||
|
// the base path before calling the base Fs.
|
||||||
|
// Any file name (after filepath.Clean()) outside this base path will be
|
||||||
|
// treated as non existing file.
|
||||||
|
//
|
||||||
|
// Note that it does not clean the error messages on return, so you may
|
||||||
|
// reveal the real path on errors.
|
||||||
|
type BasePathFs struct {
|
||||||
|
source Fs
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasePathFile struct {
|
||||||
|
File
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *BasePathFile) Name() string {
|
||||||
|
sourcename := f.File.Name()
|
||||||
|
return strings.TrimPrefix(sourcename, filepath.Clean(f.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBasePathFs(source Fs, path string) Fs {
|
||||||
|
return &BasePathFs{source: source, path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
// on a file outside the base path it returns the given file name and an error,
|
||||||
|
// else the given file with the base path prepended
|
||||||
|
func (b *BasePathFs) RealPath(name string) (path string, err error) {
|
||||||
|
if err := validateBasePathName(name); err != nil {
|
||||||
|
return name, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bpath := filepath.Clean(b.path)
|
||||||
|
path = filepath.Clean(filepath.Join(bpath, name))
|
||||||
|
if !strings.HasPrefix(path, bpath) {
|
||||||
|
return name, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateBasePathName(name string) error {
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
// Not much to do here;
|
||||||
|
// the virtual file paths all look absolute on *nix.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows a common mistake would be to provide an absolute OS path
|
||||||
|
// We could strip out the base part, but that would not be very portable.
|
||||||
|
if filepath.IsAbs(name) {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Chtimes(name string, atime, mtime time.Time) (err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return &os.PathError{Op: "chtimes", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Chmod(name string, mode os.FileMode) (err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return &os.PathError{Op: "chmod", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Name() string {
|
||||||
|
return "BasePathFs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Stat(name string) (fi os.FileInfo, err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return nil, &os.PathError{Op: "stat", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.Stat(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Rename(oldname, newname string) (err error) {
|
||||||
|
if oldname, err = b.RealPath(oldname); err != nil {
|
||||||
|
return &os.PathError{Op: "rename", Path: oldname, Err: err}
|
||||||
|
}
|
||||||
|
if newname, err = b.RealPath(newname); err != nil {
|
||||||
|
return &os.PathError{Op: "rename", Path: newname, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) RemoveAll(name string) (err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return &os.PathError{Op: "remove_all", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.RemoveAll(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Remove(name string) (err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return &os.PathError{Op: "remove", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) OpenFile(name string, flag int, mode os.FileMode) (f File, err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return nil, &os.PathError{Op: "openfile", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
sourcef, err := b.source.OpenFile(name, flag, mode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &BasePathFile{sourcef, b.path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Open(name string) (f File, err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return nil, &os.PathError{Op: "open", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
sourcef, err := b.source.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &BasePathFile{File: sourcef, path: b.path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Mkdir(name string, mode os.FileMode) (err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.Mkdir(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) MkdirAll(name string, mode os.FileMode) (err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return &os.PathError{Op: "mkdir", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
return b.source.MkdirAll(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) Create(name string) (f File, err error) {
|
||||||
|
if name, err = b.RealPath(name); err != nil {
|
||||||
|
return nil, &os.PathError{Op: "create", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
sourcef, err := b.source.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &BasePathFile{File: sourcef, path: b.path}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasePathFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
|
name, err := b.RealPath(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, &os.PathError{Op: "lstat", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
if lstater, ok := b.source.(Lstater); ok {
|
||||||
|
return lstater.LstatIfPossible(name)
|
||||||
|
}
|
||||||
|
fi, err := b.source.Stat(name)
|
||||||
|
return fi, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// vim: ts=4 sw=4 noexpandtab nolist syn=go
|
||||||
|
|
@ -0,0 +1,290 @@
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the cache duration is 0, cache time will be unlimited, i.e. once
|
||||||
|
// a file is in the layer, the base will never be read again for this file.
|
||||||
|
//
|
||||||
|
// For cache times greater than 0, the modification time of a file is
|
||||||
|
// checked. Note that a lot of file system implementations only allow a
|
||||||
|
// resolution of a second for timestamps... or as the godoc for os.Chtimes()
|
||||||
|
// states: "The underlying filesystem may truncate or round the values to a
|
||||||
|
// less precise time unit."
|
||||||
|
//
|
||||||
|
// This caching union will forward all write calls also to the base file
|
||||||
|
// system first. To prevent writing to the base Fs, wrap it in a read-only
|
||||||
|
// filter - Note: this will also make the overlay read-only, for writing files
|
||||||
|
// in the overlay, use the overlay Fs directly, not via the union Fs.
|
||||||
|
type CacheOnReadFs struct {
|
||||||
|
base Fs
|
||||||
|
layer Fs
|
||||||
|
cacheTime time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCacheOnReadFs(base Fs, layer Fs, cacheTime time.Duration) Fs {
|
||||||
|
return &CacheOnReadFs{base: base, layer: layer, cacheTime: cacheTime}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cacheState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// not present in the overlay, unknown if it exists in the base:
|
||||||
|
cacheMiss cacheState = iota
|
||||||
|
// present in the overlay and in base, base file is newer:
|
||||||
|
cacheStale
|
||||||
|
// present in the overlay - with cache time == 0 it may exist in the base,
|
||||||
|
// with cacheTime > 0 it exists in the base and is same age or newer in the
|
||||||
|
// overlay
|
||||||
|
cacheHit
|
||||||
|
// happens if someone writes directly to the overlay without
|
||||||
|
// going through this union
|
||||||
|
cacheLocal
|
||||||
|
)
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) cacheStatus(name string) (state cacheState, fi os.FileInfo, err error) {
|
||||||
|
var lfi, bfi os.FileInfo
|
||||||
|
lfi, err = u.layer.Stat(name)
|
||||||
|
if err == nil {
|
||||||
|
if u.cacheTime == 0 {
|
||||||
|
return cacheHit, lfi, nil
|
||||||
|
}
|
||||||
|
if lfi.ModTime().Add(u.cacheTime).Before(time.Now()) {
|
||||||
|
bfi, err = u.base.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return cacheLocal, lfi, nil
|
||||||
|
}
|
||||||
|
if bfi.ModTime().After(lfi.ModTime()) {
|
||||||
|
return cacheStale, bfi, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cacheHit, lfi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == syscall.ENOENT || os.IsNotExist(err) {
|
||||||
|
return cacheMiss, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheMiss, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) copyToLayer(name string) error {
|
||||||
|
return copyToLayer(u.base, u.layer, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Chtimes(name string, atime, mtime time.Time) error {
|
||||||
|
st, _, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheLocal:
|
||||||
|
case cacheHit:
|
||||||
|
err = u.base.Chtimes(name, atime, mtime)
|
||||||
|
case cacheStale, cacheMiss:
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = u.base.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Chmod(name string, mode os.FileMode) error {
|
||||||
|
st, _, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheLocal:
|
||||||
|
case cacheHit:
|
||||||
|
err = u.base.Chmod(name, mode)
|
||||||
|
case cacheStale, cacheMiss:
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = u.base.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
st, fi, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheMiss:
|
||||||
|
return u.base.Stat(name)
|
||||||
|
default: // cacheStale has base, cacheHit and cacheLocal the layer os.FileInfo
|
||||||
|
return fi, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Rename(oldname, newname string) error {
|
||||||
|
st, _, err := u.cacheStatus(oldname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheLocal:
|
||||||
|
case cacheHit:
|
||||||
|
err = u.base.Rename(oldname, newname)
|
||||||
|
case cacheStale, cacheMiss:
|
||||||
|
if err := u.copyToLayer(oldname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = u.base.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Remove(name string) error {
|
||||||
|
st, _, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheLocal:
|
||||||
|
case cacheHit, cacheStale, cacheMiss:
|
||||||
|
err = u.base.Remove(name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) RemoveAll(name string) error {
|
||||||
|
st, _, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheLocal:
|
||||||
|
case cacheHit, cacheStale, cacheMiss:
|
||||||
|
err = u.base.RemoveAll(name)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.RemoveAll(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
st, _, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
switch st {
|
||||||
|
case cacheLocal, cacheHit:
|
||||||
|
default:
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flag&(os.O_WRONLY|syscall.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 {
|
||||||
|
bfi, err := u.base.OpenFile(name, flag, perm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lfi, err := u.layer.OpenFile(name, flag, perm)
|
||||||
|
if err != nil {
|
||||||
|
bfi.Close() // oops, what if O_TRUNC was set and file opening in the layer failed...?
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &UnionFile{Base: bfi, Layer: lfi}, nil
|
||||||
|
}
|
||||||
|
return u.layer.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Open(name string) (File, error) {
|
||||||
|
st, fi, err := u.cacheStatus(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch st {
|
||||||
|
case cacheLocal:
|
||||||
|
return u.layer.Open(name)
|
||||||
|
|
||||||
|
case cacheMiss:
|
||||||
|
bfi, err := u.base.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if bfi.IsDir() {
|
||||||
|
return u.base.Open(name)
|
||||||
|
}
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u.layer.Open(name)
|
||||||
|
|
||||||
|
case cacheStale:
|
||||||
|
if !fi.IsDir() {
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u.layer.Open(name)
|
||||||
|
}
|
||||||
|
case cacheHit:
|
||||||
|
if !fi.IsDir() {
|
||||||
|
return u.layer.Open(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// the dirs from cacheHit, cacheStale fall down here:
|
||||||
|
bfile, _ := u.base.Open(name)
|
||||||
|
lfile, err := u.layer.Open(name)
|
||||||
|
if err != nil && bfile == nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &UnionFile{Base: bfile, Layer: lfile}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Mkdir(name string, perm os.FileMode) error {
|
||||||
|
err := u.base.Mkdir(name, perm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.MkdirAll(name, perm) // yes, MkdirAll... we cannot assume it exists in the cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Name() string {
|
||||||
|
return "CacheOnReadFs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) MkdirAll(name string, perm os.FileMode) error {
|
||||||
|
err := u.base.MkdirAll(name, perm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return u.layer.MkdirAll(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CacheOnReadFs) Create(name string) (File, error) {
|
||||||
|
bfh, err := u.base.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lfh, err := u.layer.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
// oops, see comment about OS_TRUNC above, should we remove? then we have to
|
||||||
|
// remember if the file did not exist before
|
||||||
|
bfh.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &UnionFile{Base: bfh, Layer: lfh}, nil
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright © 2016 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
// +build darwin openbsd freebsd netbsd dragonfly
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BADFD = syscall.EBADF
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright © 2016 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
// +build !darwin
|
||||||
|
// +build !openbsd
|
||||||
|
// +build !freebsd
|
||||||
|
// +build !dragonfly
|
||||||
|
// +build !netbsd
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
const BADFD = syscall.EBADFD
|
||||||
|
|
@ -0,0 +1,293 @@
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Lstater = (*CopyOnWriteFs)(nil)
|
||||||
|
|
||||||
|
// The CopyOnWriteFs is a union filesystem: a read only base file system with
|
||||||
|
// a possibly writeable layer on top. Changes to the file system will only
|
||||||
|
// be made in the overlay: Changing an existing file in the base layer which
|
||||||
|
// is not present in the overlay will copy the file to the overlay ("changing"
|
||||||
|
// includes also calls to e.g. Chtimes() and Chmod()).
|
||||||
|
//
|
||||||
|
// Reading directories is currently only supported via Open(), not OpenFile().
|
||||||
|
type CopyOnWriteFs struct {
|
||||||
|
base Fs
|
||||||
|
layer Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCopyOnWriteFs(base Fs, layer Fs) Fs {
|
||||||
|
return &CopyOnWriteFs{base: base, layer: layer}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if the file is not in the overlay
|
||||||
|
func (u *CopyOnWriteFs) isBaseFile(name string) (bool, error) {
|
||||||
|
if _, err := u.layer.Stat(name); err == nil {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
_, err := u.base.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
if oerr, ok := err.(*os.PathError); ok {
|
||||||
|
if oerr.Err == os.ErrNotExist || oerr.Err == syscall.ENOENT || oerr.Err == syscall.ENOTDIR {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == syscall.ENOENT {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) copyToLayer(name string) error {
|
||||||
|
return copyToLayer(u.base, u.layer, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) Chtimes(name string, atime, mtime time.Time) error {
|
||||||
|
b, err := u.isBaseFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b {
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return u.layer.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) Chmod(name string, mode os.FileMode) error {
|
||||||
|
b, err := u.isBaseFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b {
|
||||||
|
if err := u.copyToLayer(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return u.layer.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
fi, err := u.layer.Stat(name)
|
||||||
|
if err != nil {
|
||||||
|
isNotExist := u.isNotExist(err)
|
||||||
|
if isNotExist {
|
||||||
|
return u.base.Stat(name)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return fi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
|
llayer, ok1 := u.layer.(Lstater)
|
||||||
|
lbase, ok2 := u.base.(Lstater)
|
||||||
|
|
||||||
|
if ok1 {
|
||||||
|
fi, b, err := llayer.LstatIfPossible(name)
|
||||||
|
if err == nil {
|
||||||
|
return fi, b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !u.isNotExist(err) {
|
||||||
|
return nil, b, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok2 {
|
||||||
|
fi, b, err := lbase.LstatIfPossible(name)
|
||||||
|
if err == nil {
|
||||||
|
return fi, b, nil
|
||||||
|
}
|
||||||
|
if !u.isNotExist(err) {
|
||||||
|
return nil, b, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fi, err := u.Stat(name)
|
||||||
|
|
||||||
|
return fi, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) isNotExist(err error) bool {
|
||||||
|
if e, ok := err.(*os.PathError); ok {
|
||||||
|
err = e.Err
|
||||||
|
}
|
||||||
|
if err == os.ErrNotExist || err == syscall.ENOENT || err == syscall.ENOTDIR {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Renaming files present only in the base layer is not permitted
|
||||||
|
func (u *CopyOnWriteFs) Rename(oldname, newname string) error {
|
||||||
|
b, err := u.isBaseFile(oldname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if b {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
return u.layer.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removing files present only in the base layer is not permitted. If
|
||||||
|
// a file is present in the base layer and the overlay, only the overlay
|
||||||
|
// will be removed.
|
||||||
|
func (u *CopyOnWriteFs) Remove(name string) error {
|
||||||
|
err := u.layer.Remove(name)
|
||||||
|
switch err {
|
||||||
|
case syscall.ENOENT:
|
||||||
|
_, err = u.base.Stat(name)
|
||||||
|
if err == nil {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
return syscall.ENOENT
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) RemoveAll(name string) error {
|
||||||
|
err := u.layer.RemoveAll(name)
|
||||||
|
switch err {
|
||||||
|
case syscall.ENOENT:
|
||||||
|
_, err = u.base.Stat(name)
|
||||||
|
if err == nil {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
return syscall.ENOENT
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
b, err := u.isBaseFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if flag&(os.O_WRONLY|os.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 {
|
||||||
|
if b {
|
||||||
|
if err = u.copyToLayer(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u.layer.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := filepath.Dir(name)
|
||||||
|
isaDir, err := IsDir(u.base, dir)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if isaDir {
|
||||||
|
if err = u.layer.MkdirAll(dir, 0777); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return u.layer.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
isaDir, err = IsDir(u.layer, dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if isaDir {
|
||||||
|
return u.layer.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, &os.PathError{Op: "open", Path: name, Err: syscall.ENOTDIR} // ...or os.ErrNotExist?
|
||||||
|
}
|
||||||
|
if b {
|
||||||
|
return u.base.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
return u.layer.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function handles the 9 different possibilities caused
|
||||||
|
// by the union which are the intersection of the following...
|
||||||
|
// layer: doesn't exist, exists as a file, and exists as a directory
|
||||||
|
// base: doesn't exist, exists as a file, and exists as a directory
|
||||||
|
func (u *CopyOnWriteFs) Open(name string) (File, error) {
|
||||||
|
// Since the overlay overrides the base we check that first
|
||||||
|
b, err := u.isBaseFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overlay doesn't exist, return the base (base state irrelevant)
|
||||||
|
if b {
|
||||||
|
return u.base.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If overlay is a file, return it (base state irrelevant)
|
||||||
|
dir, err := IsDir(u.layer, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !dir {
|
||||||
|
return u.layer.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overlay is a directory, base state now matters.
|
||||||
|
// Base state has 3 states to check but 2 outcomes:
|
||||||
|
// A. It's a file or non-readable in the base (return just the overlay)
|
||||||
|
// B. It's an accessible directory in the base (return a UnionFile)
|
||||||
|
|
||||||
|
// If base is file or nonreadable, return overlay
|
||||||
|
dir, err = IsDir(u.base, name)
|
||||||
|
if !dir || err != nil {
|
||||||
|
return u.layer.Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both base & layer are directories
|
||||||
|
// Return union file (if opens are without error)
|
||||||
|
bfile, bErr := u.base.Open(name)
|
||||||
|
lfile, lErr := u.layer.Open(name)
|
||||||
|
|
||||||
|
// If either have errors at this point something is very wrong. Return nil and the errors
|
||||||
|
if bErr != nil || lErr != nil {
|
||||||
|
return nil, fmt.Errorf("BaseErr: %v\nOverlayErr: %v", bErr, lErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UnionFile{Base: bfile, Layer: lfile}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) Mkdir(name string, perm os.FileMode) error {
|
||||||
|
dir, err := IsDir(u.base, name)
|
||||||
|
if err != nil {
|
||||||
|
return u.layer.MkdirAll(name, perm)
|
||||||
|
}
|
||||||
|
if dir {
|
||||||
|
return ErrFileExists
|
||||||
|
}
|
||||||
|
return u.layer.MkdirAll(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) Name() string {
|
||||||
|
return "CopyOnWriteFs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) MkdirAll(name string, perm os.FileMode) error {
|
||||||
|
dir, err := IsDir(u.base, name)
|
||||||
|
if err != nil {
|
||||||
|
return u.layer.MkdirAll(name, perm)
|
||||||
|
}
|
||||||
|
if dir {
|
||||||
|
// This is in line with how os.MkdirAll behaves.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return u.layer.MkdirAll(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *CopyOnWriteFs) Create(name string) (File, error) {
|
||||||
|
return u.OpenFile(name, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type httpDir struct {
|
||||||
|
basePath string
|
||||||
|
fs HttpFs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d httpDir) Open(name string) (http.File, error) {
|
||||||
|
if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
|
||||||
|
strings.Contains(name, "\x00") {
|
||||||
|
return nil, errors.New("http: invalid character in file path")
|
||||||
|
}
|
||||||
|
dir := string(d.basePath)
|
||||||
|
if dir == "" {
|
||||||
|
dir = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := d.fs.Open(filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpFs struct {
|
||||||
|
source Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpFs(source Fs) *HttpFs {
|
||||||
|
return &HttpFs{source: source}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Dir(s string) *httpDir {
|
||||||
|
return &httpDir{basePath: s, fs: h}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Name() string { return "h HttpFs" }
|
||||||
|
|
||||||
|
func (h HttpFs) Create(name string) (File, error) {
|
||||||
|
return h.source.Create(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Chmod(name string, mode os.FileMode) error {
|
||||||
|
return h.source.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||||
|
return h.source.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Mkdir(name string, perm os.FileMode) error {
|
||||||
|
return h.source.Mkdir(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) MkdirAll(path string, perm os.FileMode) error {
|
||||||
|
return h.source.MkdirAll(path, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Open(name string) (http.File, error) {
|
||||||
|
f, err := h.source.Open(name)
|
||||||
|
if err == nil {
|
||||||
|
if httpfile, ok := f.(http.File); ok {
|
||||||
|
return httpfile, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
return h.source.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Remove(name string) error {
|
||||||
|
return h.source.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) RemoveAll(path string) error {
|
||||||
|
return h.source.RemoveAll(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Rename(oldname, newname string) error {
|
||||||
|
return h.source.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h HttpFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
return h.source.Stat(name)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
// Copyright ©2015 The Go Authors
|
||||||
|
// Copyright ©2015 Steve Francia <spf@spf13.com>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// byName implements sort.Interface.
|
||||||
|
type byName []os.FileInfo
|
||||||
|
|
||||||
|
func (f byName) Len() int { return len(f) }
|
||||||
|
func (f byName) Less(i, j int) bool { return f[i].Name() < f[j].Name() }
|
||||||
|
func (f byName) Swap(i, j int) { f[i], f[j] = f[j], f[i] }
|
||||||
|
|
||||||
|
// ReadDir reads the directory named by dirname and returns
|
||||||
|
// a list of sorted directory entries.
|
||||||
|
func (a Afero) ReadDir(dirname string) ([]os.FileInfo, error) {
|
||||||
|
return ReadDir(a.Fs, dirname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadDir(fs Fs, dirname string) ([]os.FileInfo, error) {
|
||||||
|
f, err := fs.Open(dirname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
list, err := f.Readdir(-1)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Sort(byName(list))
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFile reads the file named by filename and returns the contents.
|
||||||
|
// A successful call returns err == nil, not err == EOF. Because ReadFile
|
||||||
|
// reads the whole file, it does not treat an EOF from Read as an error
|
||||||
|
// to be reported.
|
||||||
|
func (a Afero) ReadFile(filename string) ([]byte, error) {
|
||||||
|
return ReadFile(a.Fs, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadFile(fs Fs, filename string) ([]byte, error) {
|
||||||
|
f, err := fs.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
// It's a good but not certain bet that FileInfo will tell us exactly how much to
|
||||||
|
// read, so let's try it but be prepared for the answer to be wrong.
|
||||||
|
var n int64
|
||||||
|
|
||||||
|
if fi, err := f.Stat(); err == nil {
|
||||||
|
// Don't preallocate a huge buffer, just in case.
|
||||||
|
if size := fi.Size(); size < 1e9 {
|
||||||
|
n = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// As initial capacity for readAll, use n + a little extra in case Size is zero,
|
||||||
|
// and to avoid another allocation after Read has filled the buffer. The readAll
|
||||||
|
// call will read into its allocated internal buffer cheaply. If the size was
|
||||||
|
// wrong, we'll either waste some space off the end or reallocate as needed, but
|
||||||
|
// in the overwhelmingly common case we'll get it just right.
|
||||||
|
return readAll(f, n+bytes.MinRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readAll reads from r until an error or EOF and returns the data it read
|
||||||
|
// from the internal buffer allocated with a specified capacity.
|
||||||
|
func readAll(r io.Reader, capacity int64) (b []byte, err error) {
|
||||||
|
buf := bytes.NewBuffer(make([]byte, 0, capacity))
|
||||||
|
// If the buffer overflows, we will get bytes.ErrTooLarge.
|
||||||
|
// Return that as an error. Any other panic remains.
|
||||||
|
defer func() {
|
||||||
|
e := recover()
|
||||||
|
if e == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if panicErr, ok := e.(error); ok && panicErr == bytes.ErrTooLarge {
|
||||||
|
err = panicErr
|
||||||
|
} else {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = buf.ReadFrom(r)
|
||||||
|
return buf.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAll reads from r until an error or EOF and returns the data it read.
|
||||||
|
// A successful call returns err == nil, not err == EOF. Because ReadAll is
|
||||||
|
// defined to read from src until EOF, it does not treat an EOF from Read
|
||||||
|
// as an error to be reported.
|
||||||
|
func ReadAll(r io.Reader) ([]byte, error) {
|
||||||
|
return readAll(r, bytes.MinRead)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFile writes data to a file named by filename.
|
||||||
|
// If the file does not exist, WriteFile creates it with permissions perm;
|
||||||
|
// otherwise WriteFile truncates it before writing.
|
||||||
|
func (a Afero) WriteFile(filename string, data []byte, perm os.FileMode) error {
|
||||||
|
return WriteFile(a.Fs, filename, data, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteFile(fs Fs, filename string, data []byte, perm os.FileMode) error {
|
||||||
|
f, err := fs.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, err := f.Write(data)
|
||||||
|
if err == nil && n < len(data) {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
if err1 := f.Close(); err == nil {
|
||||||
|
err = err1
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random number state.
|
||||||
|
// We generate random temporary file names so that there's a good
|
||||||
|
// chance the file doesn't exist yet - keeps the number of tries in
|
||||||
|
// TempFile to a minimum.
|
||||||
|
var rand uint32
|
||||||
|
var randmu sync.Mutex
|
||||||
|
|
||||||
|
func reseed() uint32 {
|
||||||
|
return uint32(time.Now().UnixNano() + int64(os.Getpid()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextSuffix() string {
|
||||||
|
randmu.Lock()
|
||||||
|
r := rand
|
||||||
|
if r == 0 {
|
||||||
|
r = reseed()
|
||||||
|
}
|
||||||
|
r = r*1664525 + 1013904223 // constants from Numerical Recipes
|
||||||
|
rand = r
|
||||||
|
randmu.Unlock()
|
||||||
|
return strconv.Itoa(int(1e9 + r%1e9))[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TempFile creates a new temporary file in the directory dir
|
||||||
|
// with a name beginning with prefix, opens the file for reading
|
||||||
|
// and writing, and returns the resulting *File.
|
||||||
|
// If dir is the empty string, TempFile uses the default directory
|
||||||
|
// for temporary files (see os.TempDir).
|
||||||
|
// Multiple programs calling TempFile simultaneously
|
||||||
|
// will not choose the same file. The caller can use f.Name()
|
||||||
|
// to find the pathname of the file. It is the caller's responsibility
|
||||||
|
// to remove the file when no longer needed.
|
||||||
|
func (a Afero) TempFile(dir, prefix string) (f File, err error) {
|
||||||
|
return TempFile(a.Fs, dir, prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TempFile(fs Fs, dir, prefix string) (f File, err error) {
|
||||||
|
if dir == "" {
|
||||||
|
dir = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
nconflict := 0
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
name := filepath.Join(dir, prefix+nextSuffix())
|
||||||
|
f, err = fs.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||||
|
if os.IsExist(err) {
|
||||||
|
if nconflict++; nconflict > 10 {
|
||||||
|
randmu.Lock()
|
||||||
|
rand = reseed()
|
||||||
|
randmu.Unlock()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TempDir creates a new temporary directory in the directory dir
|
||||||
|
// with a name beginning with prefix and returns the path of the
|
||||||
|
// new directory. If dir is the empty string, TempDir uses the
|
||||||
|
// default directory for temporary files (see os.TempDir).
|
||||||
|
// Multiple programs calling TempDir simultaneously
|
||||||
|
// will not choose the same directory. It is the caller's responsibility
|
||||||
|
// to remove the directory when no longer needed.
|
||||||
|
func (a Afero) TempDir(dir, prefix string) (name string, err error) {
|
||||||
|
return TempDir(a.Fs, dir, prefix)
|
||||||
|
}
|
||||||
|
func TempDir(fs Fs, dir, prefix string) (name string, err error) {
|
||||||
|
if dir == "" {
|
||||||
|
dir = os.TempDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
nconflict := 0
|
||||||
|
for i := 0; i < 10000; i++ {
|
||||||
|
try := filepath.Join(dir, prefix+nextSuffix())
|
||||||
|
err = fs.Mkdir(try, 0700)
|
||||||
|
if os.IsExist(err) {
|
||||||
|
if nconflict++; nconflict > 10 {
|
||||||
|
randmu.Lock()
|
||||||
|
rand = reseed()
|
||||||
|
randmu.Unlock()
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
name = try
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright © 2018 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Lstater is an optional interface in Afero. It is only implemented by the
|
||||||
|
// filesystems saying so.
|
||||||
|
// It will call Lstat if the filesystem iself is, or it delegates to, the os filesystem.
|
||||||
|
// Else it will call Stat.
|
||||||
|
// In addtion to the FileInfo, it will return a boolean telling whether Lstat was called or not.
|
||||||
|
type Lstater interface {
|
||||||
|
LstatIfPossible(name string) (os.FileInfo, bool, error)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
||||||
|
// Copyright 2009 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Glob returns the names of all files matching pattern or nil
|
||||||
|
// if there is no matching file. The syntax of patterns is the same
|
||||||
|
// as in Match. The pattern may describe hierarchical names such as
|
||||||
|
// /usr/*/bin/ed (assuming the Separator is '/').
|
||||||
|
//
|
||||||
|
// Glob ignores file system errors such as I/O errors reading directories.
|
||||||
|
// The only possible returned error is ErrBadPattern, when pattern
|
||||||
|
// is malformed.
|
||||||
|
//
|
||||||
|
// This was adapted from (http://golang.org/pkg/path/filepath) and uses several
|
||||||
|
// built-ins from that package.
|
||||||
|
func Glob(fs Fs, pattern string) (matches []string, err error) {
|
||||||
|
if !hasMeta(pattern) {
|
||||||
|
// Lstat not supported by a ll filesystems.
|
||||||
|
if _, err = lstatIfPossible(fs, pattern); err != nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return []string{pattern}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, file := filepath.Split(pattern)
|
||||||
|
switch dir {
|
||||||
|
case "":
|
||||||
|
dir = "."
|
||||||
|
case string(filepath.Separator):
|
||||||
|
// nothing
|
||||||
|
default:
|
||||||
|
dir = dir[0 : len(dir)-1] // chop off trailing separator
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasMeta(dir) {
|
||||||
|
return glob(fs, dir, file, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var m []string
|
||||||
|
m, err = Glob(fs, dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, d := range m {
|
||||||
|
matches, err = glob(fs, d, file, matches)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// glob searches for files matching pattern in the directory dir
|
||||||
|
// and appends them to matches. If the directory cannot be
|
||||||
|
// opened, it returns the existing matches. New matches are
|
||||||
|
// added in lexicographical order.
|
||||||
|
func glob(fs Fs, dir, pattern string, matches []string) (m []string, e error) {
|
||||||
|
m = matches
|
||||||
|
fi, err := fs.Stat(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !fi.IsDir() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d, err := fs.Open(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer d.Close()
|
||||||
|
|
||||||
|
names, _ := d.Readdirnames(-1)
|
||||||
|
sort.Strings(names)
|
||||||
|
|
||||||
|
for _, n := range names {
|
||||||
|
matched, err := filepath.Match(pattern, n)
|
||||||
|
if err != nil {
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
if matched {
|
||||||
|
m = append(m, filepath.Join(dir, n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// hasMeta reports whether path contains any of the magic characters
|
||||||
|
// recognized by Match.
|
||||||
|
func hasMeta(path string) bool {
|
||||||
|
// TODO(niemeyer): Should other magic characters be added here?
|
||||||
|
return strings.IndexAny(path, "*?[") >= 0
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mem
|
||||||
|
|
||||||
|
type Dir interface {
|
||||||
|
Len() int
|
||||||
|
Names() []string
|
||||||
|
Files() []*FileData
|
||||||
|
Add(*FileData)
|
||||||
|
Remove(*FileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveFromMemDir(dir *FileData, f *FileData) {
|
||||||
|
dir.memDir.Remove(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddToMemDir(dir *FileData, f *FileData) {
|
||||||
|
dir.memDir.Add(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitializeDir(d *FileData) {
|
||||||
|
if d.memDir == nil {
|
||||||
|
d.dir = true
|
||||||
|
d.memDir = &DirMap{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// Copyright © 2015 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mem
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
type DirMap map[string]*FileData
|
||||||
|
|
||||||
|
func (m DirMap) Len() int { return len(m) }
|
||||||
|
func (m DirMap) Add(f *FileData) { m[f.name] = f }
|
||||||
|
func (m DirMap) Remove(f *FileData) { delete(m, f.name) }
|
||||||
|
func (m DirMap) Files() (files []*FileData) {
|
||||||
|
for _, f := range m {
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
sort.Sort(filesSorter(files))
|
||||||
|
return files
|
||||||
|
}
|
||||||
|
|
||||||
|
// implement sort.Interface for []*FileData
|
||||||
|
type filesSorter []*FileData
|
||||||
|
|
||||||
|
func (s filesSorter) Len() int { return len(s) }
|
||||||
|
func (s filesSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||||
|
func (s filesSorter) Less(i, j int) bool { return s[i].name < s[j].name }
|
||||||
|
|
||||||
|
func (m DirMap) Names() (names []string) {
|
||||||
|
for x := range m {
|
||||||
|
names = append(names, x)
|
||||||
|
}
|
||||||
|
return names
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,317 @@
|
||||||
|
// Copyright © 2015 Steve Francia <spf@spf13.com>.
|
||||||
|
// Copyright 2013 tsuru authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package mem
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
)
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
const FilePathSeparator = string(filepath.Separator)
|
||||||
|
|
||||||
|
type File struct {
|
||||||
|
// atomic requires 64-bit alignment for struct field access
|
||||||
|
at int64
|
||||||
|
readDirCount int64
|
||||||
|
closed bool
|
||||||
|
readOnly bool
|
||||||
|
fileData *FileData
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFileHandle(data *FileData) *File {
|
||||||
|
return &File{fileData: data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReadOnlyFileHandle(data *FileData) *File {
|
||||||
|
return &File{fileData: data, readOnly: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f File) Data() *FileData {
|
||||||
|
return f.fileData
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileData struct {
|
||||||
|
sync.Mutex
|
||||||
|
name string
|
||||||
|
data []byte
|
||||||
|
memDir Dir
|
||||||
|
dir bool
|
||||||
|
mode os.FileMode
|
||||||
|
modtime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *FileData) Name() string {
|
||||||
|
d.Lock()
|
||||||
|
defer d.Unlock()
|
||||||
|
return d.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateFile(name string) *FileData {
|
||||||
|
return &FileData{name: name, mode: os.ModeTemporary, modtime: time.Now()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateDir(name string) *FileData {
|
||||||
|
return &FileData{name: name, memDir: &DirMap{}, dir: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangeFileName(f *FileData, newname string) {
|
||||||
|
f.Lock()
|
||||||
|
f.name = newname
|
||||||
|
f.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetMode(f *FileData, mode os.FileMode) {
|
||||||
|
f.Lock()
|
||||||
|
f.mode = mode
|
||||||
|
f.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetModTime(f *FileData, mtime time.Time) {
|
||||||
|
f.Lock()
|
||||||
|
setModTime(f, mtime)
|
||||||
|
f.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setModTime(f *FileData, mtime time.Time) {
|
||||||
|
f.modtime = mtime
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetFileInfo(f *FileData) *FileInfo {
|
||||||
|
return &FileInfo{f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Open() error {
|
||||||
|
atomic.StoreInt64(&f.at, 0)
|
||||||
|
atomic.StoreInt64(&f.readDirCount, 0)
|
||||||
|
f.fileData.Lock()
|
||||||
|
f.closed = false
|
||||||
|
f.fileData.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Close() error {
|
||||||
|
f.fileData.Lock()
|
||||||
|
f.closed = true
|
||||||
|
if !f.readOnly {
|
||||||
|
setModTime(f.fileData, time.Now())
|
||||||
|
}
|
||||||
|
f.fileData.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Name() string {
|
||||||
|
return f.fileData.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Stat() (os.FileInfo, error) {
|
||||||
|
return &FileInfo{f.fileData}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Sync() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Readdir(count int) (res []os.FileInfo, err error) {
|
||||||
|
if !f.fileData.dir {
|
||||||
|
return nil, &os.PathError{Op: "readdir", Path: f.fileData.name, Err: errors.New("not a dir")}
|
||||||
|
}
|
||||||
|
var outLength int64
|
||||||
|
|
||||||
|
f.fileData.Lock()
|
||||||
|
files := f.fileData.memDir.Files()[f.readDirCount:]
|
||||||
|
if count > 0 {
|
||||||
|
if len(files) < count {
|
||||||
|
outLength = int64(len(files))
|
||||||
|
} else {
|
||||||
|
outLength = int64(count)
|
||||||
|
}
|
||||||
|
if len(files) == 0 {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outLength = int64(len(files))
|
||||||
|
}
|
||||||
|
f.readDirCount += outLength
|
||||||
|
f.fileData.Unlock()
|
||||||
|
|
||||||
|
res = make([]os.FileInfo, outLength)
|
||||||
|
for i := range res {
|
||||||
|
res[i] = &FileInfo{files[i]}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Readdirnames(n int) (names []string, err error) {
|
||||||
|
fi, err := f.Readdir(n)
|
||||||
|
names = make([]string, len(fi))
|
||||||
|
for i, f := range fi {
|
||||||
|
_, names[i] = filepath.Split(f.Name())
|
||||||
|
}
|
||||||
|
return names, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Read(b []byte) (n int, err error) {
|
||||||
|
f.fileData.Lock()
|
||||||
|
defer f.fileData.Unlock()
|
||||||
|
if f.closed == true {
|
||||||
|
return 0, ErrFileClosed
|
||||||
|
}
|
||||||
|
if len(b) > 0 && int(f.at) == len(f.fileData.data) {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
if int(f.at) > len(f.fileData.data) {
|
||||||
|
return 0, io.ErrUnexpectedEOF
|
||||||
|
}
|
||||||
|
if len(f.fileData.data)-int(f.at) >= len(b) {
|
||||||
|
n = len(b)
|
||||||
|
} else {
|
||||||
|
n = len(f.fileData.data) - int(f.at)
|
||||||
|
}
|
||||||
|
copy(b, f.fileData.data[f.at:f.at+int64(n)])
|
||||||
|
atomic.AddInt64(&f.at, int64(n))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) ReadAt(b []byte, off int64) (n int, err error) {
|
||||||
|
atomic.StoreInt64(&f.at, off)
|
||||||
|
return f.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Truncate(size int64) error {
|
||||||
|
if f.closed == true {
|
||||||
|
return ErrFileClosed
|
||||||
|
}
|
||||||
|
if f.readOnly {
|
||||||
|
return &os.PathError{Op: "truncate", Path: f.fileData.name, Err: errors.New("file handle is read only")}
|
||||||
|
}
|
||||||
|
if size < 0 {
|
||||||
|
return ErrOutOfRange
|
||||||
|
}
|
||||||
|
if size > int64(len(f.fileData.data)) {
|
||||||
|
diff := size - int64(len(f.fileData.data))
|
||||||
|
f.fileData.data = append(f.fileData.data, bytes.Repeat([]byte{00}, int(diff))...)
|
||||||
|
} else {
|
||||||
|
f.fileData.data = f.fileData.data[0:size]
|
||||||
|
}
|
||||||
|
setModTime(f.fileData, time.Now())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if f.closed == true {
|
||||||
|
return 0, ErrFileClosed
|
||||||
|
}
|
||||||
|
switch whence {
|
||||||
|
case 0:
|
||||||
|
atomic.StoreInt64(&f.at, offset)
|
||||||
|
case 1:
|
||||||
|
atomic.AddInt64(&f.at, int64(offset))
|
||||||
|
case 2:
|
||||||
|
atomic.StoreInt64(&f.at, int64(len(f.fileData.data))+offset)
|
||||||
|
}
|
||||||
|
return f.at, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Write(b []byte) (n int, err error) {
|
||||||
|
if f.readOnly {
|
||||||
|
return 0, &os.PathError{Op: "write", Path: f.fileData.name, Err: errors.New("file handle is read only")}
|
||||||
|
}
|
||||||
|
n = len(b)
|
||||||
|
cur := atomic.LoadInt64(&f.at)
|
||||||
|
f.fileData.Lock()
|
||||||
|
defer f.fileData.Unlock()
|
||||||
|
diff := cur - int64(len(f.fileData.data))
|
||||||
|
var tail []byte
|
||||||
|
if n+int(cur) < len(f.fileData.data) {
|
||||||
|
tail = f.fileData.data[n+int(cur):]
|
||||||
|
}
|
||||||
|
if diff > 0 {
|
||||||
|
f.fileData.data = append(bytes.Repeat([]byte{00}, int(diff)), b...)
|
||||||
|
f.fileData.data = append(f.fileData.data, tail...)
|
||||||
|
} else {
|
||||||
|
f.fileData.data = append(f.fileData.data[:cur], b...)
|
||||||
|
f.fileData.data = append(f.fileData.data, tail...)
|
||||||
|
}
|
||||||
|
setModTime(f.fileData, time.Now())
|
||||||
|
|
||||||
|
atomic.StoreInt64(&f.at, int64(len(f.fileData.data)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) WriteAt(b []byte, off int64) (n int, err error) {
|
||||||
|
atomic.StoreInt64(&f.at, off)
|
||||||
|
return f.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) WriteString(s string) (ret int, err error) {
|
||||||
|
return f.Write([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Info() *FileInfo {
|
||||||
|
return &FileInfo{f.fileData}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileInfo struct {
|
||||||
|
*FileData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements os.FileInfo
|
||||||
|
func (s *FileInfo) Name() string {
|
||||||
|
s.Lock()
|
||||||
|
_, name := filepath.Split(s.name)
|
||||||
|
s.Unlock()
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
func (s *FileInfo) Mode() os.FileMode {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
return s.mode
|
||||||
|
}
|
||||||
|
func (s *FileInfo) ModTime() time.Time {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
return s.modtime
|
||||||
|
}
|
||||||
|
func (s *FileInfo) IsDir() bool {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
return s.dir
|
||||||
|
}
|
||||||
|
func (s *FileInfo) Sys() interface{} { return nil }
|
||||||
|
func (s *FileInfo) Size() int64 {
|
||||||
|
if s.IsDir() {
|
||||||
|
return int64(42)
|
||||||
|
}
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
return int64(len(s.data))
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFileClosed = errors.New("File is closed")
|
||||||
|
ErrOutOfRange = errors.New("Out of range")
|
||||||
|
ErrTooLarge = errors.New("Too large")
|
||||||
|
ErrFileNotFound = os.ErrNotExist
|
||||||
|
ErrFileExists = os.ErrExist
|
||||||
|
ErrDestinationExists = os.ErrExist
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/spf13/afero/mem"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MemMapFs struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
data map[string]*mem.FileData
|
||||||
|
init sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMemMapFs() Fs {
|
||||||
|
return &MemMapFs{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) getData() map[string]*mem.FileData {
|
||||||
|
m.init.Do(func() {
|
||||||
|
m.data = make(map[string]*mem.FileData)
|
||||||
|
// Root should always exist, right?
|
||||||
|
// TODO: what about windows?
|
||||||
|
m.data[FilePathSeparator] = mem.CreateDir(FilePathSeparator)
|
||||||
|
})
|
||||||
|
return m.data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*MemMapFs) Name() string { return "MemMapFS" }
|
||||||
|
|
||||||
|
func (m *MemMapFs) Create(name string) (File, error) {
|
||||||
|
name = normalizePath(name)
|
||||||
|
m.mu.Lock()
|
||||||
|
file := mem.CreateFile(name)
|
||||||
|
m.getData()[name] = file
|
||||||
|
m.registerWithParent(file)
|
||||||
|
m.mu.Unlock()
|
||||||
|
return mem.NewFileHandle(file), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) unRegisterWithParent(fileName string) error {
|
||||||
|
f, err := m.lockfreeOpen(fileName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parent := m.findParent(f)
|
||||||
|
if parent == nil {
|
||||||
|
log.Panic("parent of ", f.Name(), " is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.Lock()
|
||||||
|
mem.RemoveFromMemDir(parent, f)
|
||||||
|
parent.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) findParent(f *mem.FileData) *mem.FileData {
|
||||||
|
pdir, _ := filepath.Split(f.Name())
|
||||||
|
pdir = filepath.Clean(pdir)
|
||||||
|
pfile, err := m.lockfreeOpen(pdir)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pfile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) registerWithParent(f *mem.FileData) {
|
||||||
|
if f == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parent := m.findParent(f)
|
||||||
|
if parent == nil {
|
||||||
|
pdir := filepath.Dir(filepath.Clean(f.Name()))
|
||||||
|
err := m.lockfreeMkdir(pdir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
//log.Println("Mkdir error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
parent, err = m.lockfreeOpen(pdir)
|
||||||
|
if err != nil {
|
||||||
|
//log.Println("Open after Mkdir error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.Lock()
|
||||||
|
mem.InitializeDir(parent)
|
||||||
|
mem.AddToMemDir(parent, f)
|
||||||
|
parent.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) lockfreeMkdir(name string, perm os.FileMode) error {
|
||||||
|
name = normalizePath(name)
|
||||||
|
x, ok := m.getData()[name]
|
||||||
|
if ok {
|
||||||
|
// Only return ErrFileExists if it's a file, not a directory.
|
||||||
|
i := mem.FileInfo{FileData: x}
|
||||||
|
if !i.IsDir() {
|
||||||
|
return ErrFileExists
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item := mem.CreateDir(name)
|
||||||
|
m.getData()[name] = item
|
||||||
|
m.registerWithParent(item)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Mkdir(name string, perm os.FileMode) error {
|
||||||
|
name = normalizePath(name)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
_, ok := m.getData()[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if ok {
|
||||||
|
return &os.PathError{Op: "mkdir", Path: name, Err: ErrFileExists}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
item := mem.CreateDir(name)
|
||||||
|
m.getData()[name] = item
|
||||||
|
m.registerWithParent(item)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.Chmod(name, perm|os.ModeDir)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) MkdirAll(path string, perm os.FileMode) error {
|
||||||
|
err := m.Mkdir(path, perm)
|
||||||
|
if err != nil {
|
||||||
|
if err.(*os.PathError).Err == ErrFileExists {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle some relative paths
|
||||||
|
func normalizePath(path string) string {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
|
||||||
|
switch path {
|
||||||
|
case ".":
|
||||||
|
return FilePathSeparator
|
||||||
|
case "..":
|
||||||
|
return FilePathSeparator
|
||||||
|
default:
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Open(name string) (File, error) {
|
||||||
|
f, err := m.open(name)
|
||||||
|
if f != nil {
|
||||||
|
return mem.NewReadOnlyFileHandle(f), err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) openWrite(name string) (File, error) {
|
||||||
|
f, err := m.open(name)
|
||||||
|
if f != nil {
|
||||||
|
return mem.NewFileHandle(f), err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) open(name string) (*mem.FileData, error) {
|
||||||
|
name = normalizePath(name)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
f, ok := m.getData()[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return nil, &os.PathError{Op: "open", Path: name, Err: ErrFileNotFound}
|
||||||
|
}
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) lockfreeOpen(name string) (*mem.FileData, error) {
|
||||||
|
name = normalizePath(name)
|
||||||
|
f, ok := m.getData()[name]
|
||||||
|
if ok {
|
||||||
|
return f, nil
|
||||||
|
} else {
|
||||||
|
return nil, ErrFileNotFound
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
chmod := false
|
||||||
|
file, err := m.openWrite(name)
|
||||||
|
if os.IsNotExist(err) && (flag&os.O_CREATE > 0) {
|
||||||
|
file, err = m.Create(name)
|
||||||
|
chmod = true
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if flag == os.O_RDONLY {
|
||||||
|
file = mem.NewReadOnlyFileHandle(file.(*mem.File).Data())
|
||||||
|
}
|
||||||
|
if flag&os.O_APPEND > 0 {
|
||||||
|
_, err = file.Seek(0, os.SEEK_END)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flag&os.O_TRUNC > 0 && flag&(os.O_RDWR|os.O_WRONLY) > 0 {
|
||||||
|
err = file.Truncate(0)
|
||||||
|
if err != nil {
|
||||||
|
file.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chmod {
|
||||||
|
m.Chmod(name, perm)
|
||||||
|
}
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Remove(name string) error {
|
||||||
|
name = normalizePath(name)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
if _, ok := m.getData()[name]; ok {
|
||||||
|
err := m.unRegisterWithParent(name)
|
||||||
|
if err != nil {
|
||||||
|
return &os.PathError{Op: "remove", Path: name, Err: err}
|
||||||
|
}
|
||||||
|
delete(m.getData(), name)
|
||||||
|
} else {
|
||||||
|
return &os.PathError{Op: "remove", Path: name, Err: os.ErrNotExist}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) RemoveAll(path string) error {
|
||||||
|
path = normalizePath(path)
|
||||||
|
m.mu.Lock()
|
||||||
|
m.unRegisterWithParent(path)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
|
||||||
|
for p, _ := range m.getData() {
|
||||||
|
if strings.HasPrefix(p, path) {
|
||||||
|
m.mu.RUnlock()
|
||||||
|
m.mu.Lock()
|
||||||
|
delete(m.getData(), p)
|
||||||
|
m.mu.Unlock()
|
||||||
|
m.mu.RLock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Rename(oldname, newname string) error {
|
||||||
|
oldname = normalizePath(oldname)
|
||||||
|
newname = normalizePath(newname)
|
||||||
|
|
||||||
|
if oldname == newname {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
defer m.mu.RUnlock()
|
||||||
|
if _, ok := m.getData()[oldname]; ok {
|
||||||
|
m.mu.RUnlock()
|
||||||
|
m.mu.Lock()
|
||||||
|
m.unRegisterWithParent(oldname)
|
||||||
|
fileData := m.getData()[oldname]
|
||||||
|
delete(m.getData(), oldname)
|
||||||
|
mem.ChangeFileName(fileData, newname)
|
||||||
|
m.getData()[newname] = fileData
|
||||||
|
m.registerWithParent(fileData)
|
||||||
|
m.mu.Unlock()
|
||||||
|
m.mu.RLock()
|
||||||
|
} else {
|
||||||
|
return &os.PathError{Op: "rename", Path: oldname, Err: ErrFileNotFound}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
f, err := m.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fi := mem.GetFileInfo(f.(*mem.File).Data())
|
||||||
|
return fi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Chmod(name string, mode os.FileMode) error {
|
||||||
|
name = normalizePath(name)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
f, ok := m.getData()[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return &os.PathError{Op: "chmod", Path: name, Err: ErrFileNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
mem.SetMode(f, mode)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||||
|
name = normalizePath(name)
|
||||||
|
|
||||||
|
m.mu.RLock()
|
||||||
|
f, ok := m.getData()[name]
|
||||||
|
m.mu.RUnlock()
|
||||||
|
if !ok {
|
||||||
|
return &os.PathError{Op: "chtimes", Path: name, Err: ErrFileNotFound}
|
||||||
|
}
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
mem.SetModTime(f, mtime)
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MemMapFs) List() {
|
||||||
|
for _, x := range m.data {
|
||||||
|
y := mem.FileInfo{FileData: x}
|
||||||
|
fmt.Println(x.Name(), y.Size())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func debugMemMapList(fs Fs) {
|
||||||
|
// if x, ok := fs.(*MemMapFs); ok {
|
||||||
|
// x.List()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// Copyright © 2014 Steve Francia <spf@spf13.com>.
|
||||||
|
// Copyright 2013 tsuru authors. All rights reserved.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Lstater = (*OsFs)(nil)
|
||||||
|
|
||||||
|
// OsFs is a Fs implementation that uses functions provided by the os package.
|
||||||
|
//
|
||||||
|
// For details in any method, check the documentation of the os package
|
||||||
|
// (http://golang.org/pkg/os/).
|
||||||
|
type OsFs struct{}
|
||||||
|
|
||||||
|
func NewOsFs() Fs {
|
||||||
|
return &OsFs{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Name() string { return "OsFs" }
|
||||||
|
|
||||||
|
func (OsFs) Create(name string) (File, error) {
|
||||||
|
f, e := os.Create(name)
|
||||||
|
if f == nil {
|
||||||
|
// while this looks strange, we need to return a bare nil (of type nil) not
|
||||||
|
// a nil value of type *os.File or nil won't be nil
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return f, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Mkdir(name string, perm os.FileMode) error {
|
||||||
|
return os.Mkdir(name, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) MkdirAll(path string, perm os.FileMode) error {
|
||||||
|
return os.MkdirAll(path, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Open(name string) (File, error) {
|
||||||
|
f, e := os.Open(name)
|
||||||
|
if f == nil {
|
||||||
|
// while this looks strange, we need to return a bare nil (of type nil) not
|
||||||
|
// a nil value of type *os.File or nil won't be nil
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return f, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
f, e := os.OpenFile(name, flag, perm)
|
||||||
|
if f == nil {
|
||||||
|
// while this looks strange, we need to return a bare nil (of type nil) not
|
||||||
|
// a nil value of type *os.File or nil won't be nil
|
||||||
|
return nil, e
|
||||||
|
}
|
||||||
|
return f, e
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Remove(name string) error {
|
||||||
|
return os.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) RemoveAll(path string) error {
|
||||||
|
return os.RemoveAll(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Rename(oldname, newname string) error {
|
||||||
|
return os.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
return os.Stat(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Chmod(name string, mode os.FileMode) error {
|
||||||
|
return os.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||||
|
return os.Chtimes(name, atime, mtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (OsFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
|
fi, err := os.Lstat(name)
|
||||||
|
return fi, true, err
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Copyright ©2015 The Go Authors
|
||||||
|
// Copyright ©2015 Steve Francia <spf@spf13.com>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// readDirNames reads the directory named by dirname and returns
|
||||||
|
// a sorted list of directory entries.
|
||||||
|
// adapted from https://golang.org/src/path/filepath/path.go
|
||||||
|
func readDirNames(fs Fs, dirname string) ([]string, error) {
|
||||||
|
f, err := fs.Open(dirname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
names, err := f.Readdirnames(-1)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sort.Strings(names)
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// walk recursively descends path, calling walkFn
|
||||||
|
// adapted from https://golang.org/src/path/filepath/path.go
|
||||||
|
func walk(fs Fs, path string, info os.FileInfo, walkFn filepath.WalkFunc) error {
|
||||||
|
err := walkFn(path, info, nil)
|
||||||
|
if err != nil {
|
||||||
|
if info.IsDir() && err == filepath.SkipDir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
names, err := readDirNames(fs, path)
|
||||||
|
if err != nil {
|
||||||
|
return walkFn(path, info, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
filename := filepath.Join(path, name)
|
||||||
|
fileInfo, err := lstatIfPossible(fs, filename)
|
||||||
|
if err != nil {
|
||||||
|
if err := walkFn(filename, fileInfo, err); err != nil && err != filepath.SkipDir {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = walk(fs, filename, fileInfo, walkFn)
|
||||||
|
if err != nil {
|
||||||
|
if !fileInfo.IsDir() || err != filepath.SkipDir {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the filesystem supports it, use Lstat, else use fs.Stat
|
||||||
|
func lstatIfPossible(fs Fs, path string) (os.FileInfo, error) {
|
||||||
|
if lfs, ok := fs.(Lstater); ok {
|
||||||
|
fi, _, err := lfs.LstatIfPossible(path)
|
||||||
|
return fi, err
|
||||||
|
}
|
||||||
|
return fs.Stat(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walk walks the file tree rooted at root, calling walkFn for each file or
|
||||||
|
// directory in the tree, including root. All errors that arise visiting files
|
||||||
|
// and directories are filtered by walkFn. The files are walked in lexical
|
||||||
|
// order, which makes the output deterministic but means that for very
|
||||||
|
// large directories Walk can be inefficient.
|
||||||
|
// Walk does not follow symbolic links.
|
||||||
|
|
||||||
|
func (a Afero) Walk(root string, walkFn filepath.WalkFunc) error {
|
||||||
|
return Walk(a.Fs, root, walkFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Walk(fs Fs, root string, walkFn filepath.WalkFunc) error {
|
||||||
|
info, err := lstatIfPossible(fs, root)
|
||||||
|
if err != nil {
|
||||||
|
return walkFn(root, nil, err)
|
||||||
|
}
|
||||||
|
return walk(fs, root, info, walkFn)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Lstater = (*ReadOnlyFs)(nil)
|
||||||
|
|
||||||
|
type ReadOnlyFs struct {
|
||||||
|
source Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewReadOnlyFs(source Fs) Fs {
|
||||||
|
return &ReadOnlyFs{source: source}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) ReadDir(name string) ([]os.FileInfo, error) {
|
||||||
|
return ReadDir(r.source, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Chtimes(n string, a, m time.Time) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Chmod(n string, m os.FileMode) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Name() string {
|
||||||
|
return "ReadOnlyFilter"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
return r.source.Stat(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) LstatIfPossible(name string) (os.FileInfo, bool, error) {
|
||||||
|
if lsf, ok := r.source.(Lstater); ok {
|
||||||
|
return lsf.LstatIfPossible(name)
|
||||||
|
}
|
||||||
|
fi, err := r.Stat(name)
|
||||||
|
return fi, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Rename(o, n string) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) RemoveAll(p string) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Remove(n string) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
if flag&(os.O_WRONLY|syscall.O_RDWR|os.O_APPEND|os.O_CREATE|os.O_TRUNC) != 0 {
|
||||||
|
return nil, syscall.EPERM
|
||||||
|
}
|
||||||
|
return r.source.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Open(n string) (File, error) {
|
||||||
|
return r.source.Open(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Mkdir(n string, p os.FileMode) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) MkdirAll(n string, p os.FileMode) error {
|
||||||
|
return syscall.EPERM
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReadOnlyFs) Create(n string) (File, error) {
|
||||||
|
return nil, syscall.EPERM
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The RegexpFs filters files (not directories) by regular expression. Only
|
||||||
|
// files matching the given regexp will be allowed, all others get a ENOENT error (
|
||||||
|
// "No such file or directory").
|
||||||
|
//
|
||||||
|
type RegexpFs struct {
|
||||||
|
re *regexp.Regexp
|
||||||
|
source Fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegexpFs(source Fs, re *regexp.Regexp) Fs {
|
||||||
|
return &RegexpFs{source: source, re: re}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegexpFile struct {
|
||||||
|
f File
|
||||||
|
re *regexp.Regexp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) matchesName(name string) error {
|
||||||
|
if r.re == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.re.MatchString(name) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return syscall.ENOENT
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) dirOrMatches(name string) error {
|
||||||
|
dir, err := IsDir(r.source, name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if dir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.matchesName(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Chtimes(name string, a, m time.Time) error {
|
||||||
|
if err := r.dirOrMatches(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.source.Chtimes(name, a, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Chmod(name string, mode os.FileMode) error {
|
||||||
|
if err := r.dirOrMatches(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.source.Chmod(name, mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Name() string {
|
||||||
|
return "RegexpFs"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Stat(name string) (os.FileInfo, error) {
|
||||||
|
if err := r.dirOrMatches(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.source.Stat(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Rename(oldname, newname string) error {
|
||||||
|
dir, err := IsDir(r.source, oldname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if dir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := r.matchesName(oldname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.matchesName(newname); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.source.Rename(oldname, newname)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) RemoveAll(p string) error {
|
||||||
|
dir, err := IsDir(r.source, p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !dir {
|
||||||
|
if err := r.matchesName(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.source.RemoveAll(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Remove(name string) error {
|
||||||
|
if err := r.dirOrMatches(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.source.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
|
||||||
|
if err := r.dirOrMatches(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.source.OpenFile(name, flag, perm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Open(name string) (File, error) {
|
||||||
|
dir, err := IsDir(r.source, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !dir {
|
||||||
|
if err := r.matchesName(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f, err := r.source.Open(name)
|
||||||
|
return &RegexpFile{f: f, re: r.re}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Mkdir(n string, p os.FileMode) error {
|
||||||
|
return r.source.Mkdir(n, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) MkdirAll(n string, p os.FileMode) error {
|
||||||
|
return r.source.MkdirAll(n, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RegexpFs) Create(name string) (File, error) {
|
||||||
|
if err := r.matchesName(name); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.source.Create(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Close() error {
|
||||||
|
return f.f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Read(s []byte) (int, error) {
|
||||||
|
return f.f.Read(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) ReadAt(s []byte, o int64) (int, error) {
|
||||||
|
return f.f.ReadAt(s, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Seek(o int64, w int) (int64, error) {
|
||||||
|
return f.f.Seek(o, w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Write(s []byte) (int, error) {
|
||||||
|
return f.f.Write(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) WriteAt(s []byte, o int64) (int, error) {
|
||||||
|
return f.f.WriteAt(s, o)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Name() string {
|
||||||
|
return f.f.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Readdir(c int) (fi []os.FileInfo, err error) {
|
||||||
|
var rfi []os.FileInfo
|
||||||
|
rfi, err = f.f.Readdir(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, i := range rfi {
|
||||||
|
if i.IsDir() || f.re.MatchString(i.Name()) {
|
||||||
|
fi = append(fi, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fi, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Readdirnames(c int) (n []string, err error) {
|
||||||
|
fi, err := f.Readdir(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, s := range fi {
|
||||||
|
n = append(n, s.Name())
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Stat() (os.FileInfo, error) {
|
||||||
|
return f.f.Stat()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Sync() error {
|
||||||
|
return f.f.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) Truncate(s int64) error {
|
||||||
|
return f.f.Truncate(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *RegexpFile) WriteString(s string) (int, error) {
|
||||||
|
return f.f.WriteString(s)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,316 @@
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The UnionFile implements the afero.File interface and will be returned
|
||||||
|
// when reading a directory present at least in the overlay or opening a file
|
||||||
|
// for writing.
|
||||||
|
//
|
||||||
|
// The calls to
|
||||||
|
// Readdir() and Readdirnames() merge the file os.FileInfo / names from the
|
||||||
|
// base and the overlay - for files present in both layers, only those
|
||||||
|
// from the overlay will be used.
|
||||||
|
//
|
||||||
|
// When opening files for writing (Create() / OpenFile() with the right flags)
|
||||||
|
// the operations will be done in both layers, starting with the overlay. A
|
||||||
|
// successful read in the overlay will move the cursor position in the base layer
|
||||||
|
// by the number of bytes read.
|
||||||
|
type UnionFile struct {
|
||||||
|
Base File
|
||||||
|
Layer File
|
||||||
|
Merger DirsMerger
|
||||||
|
off int
|
||||||
|
files []os.FileInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Close() error {
|
||||||
|
// first close base, so we have a newer timestamp in the overlay. If we'd close
|
||||||
|
// the overlay first, we'd get a cacheStale the next time we access this file
|
||||||
|
// -> cache would be useless ;-)
|
||||||
|
if f.Base != nil {
|
||||||
|
f.Base.Close()
|
||||||
|
}
|
||||||
|
if f.Layer != nil {
|
||||||
|
return f.Layer.Close()
|
||||||
|
}
|
||||||
|
return BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Read(s []byte) (int, error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
n, err := f.Layer.Read(s)
|
||||||
|
if (err == nil || err == io.EOF) && f.Base != nil {
|
||||||
|
// advance the file position also in the base file, the next
|
||||||
|
// call may be a write at this position (or a seek with SEEK_CUR)
|
||||||
|
if _, seekErr := f.Base.Seek(int64(n), os.SEEK_CUR); seekErr != nil {
|
||||||
|
// only overwrite err in case the seek fails: we need to
|
||||||
|
// report an eventual io.EOF to the caller
|
||||||
|
err = seekErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.Read(s)
|
||||||
|
}
|
||||||
|
return 0, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) ReadAt(s []byte, o int64) (int, error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
n, err := f.Layer.ReadAt(s, o)
|
||||||
|
if (err == nil || err == io.EOF) && f.Base != nil {
|
||||||
|
_, err = f.Base.Seek(o+int64(n), os.SEEK_SET)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.ReadAt(s, o)
|
||||||
|
}
|
||||||
|
return 0, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Seek(o int64, w int) (pos int64, err error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
pos, err = f.Layer.Seek(o, w)
|
||||||
|
if (err == nil || err == io.EOF) && f.Base != nil {
|
||||||
|
_, err = f.Base.Seek(o, w)
|
||||||
|
}
|
||||||
|
return pos, err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.Seek(o, w)
|
||||||
|
}
|
||||||
|
return 0, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Write(s []byte) (n int, err error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
n, err = f.Layer.Write(s)
|
||||||
|
if err == nil && f.Base != nil { // hmm, do we have fixed size files where a write may hit the EOF mark?
|
||||||
|
_, err = f.Base.Write(s)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.Write(s)
|
||||||
|
}
|
||||||
|
return 0, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) WriteAt(s []byte, o int64) (n int, err error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
n, err = f.Layer.WriteAt(s, o)
|
||||||
|
if err == nil && f.Base != nil {
|
||||||
|
_, err = f.Base.WriteAt(s, o)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.WriteAt(s, o)
|
||||||
|
}
|
||||||
|
return 0, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Name() string {
|
||||||
|
if f.Layer != nil {
|
||||||
|
return f.Layer.Name()
|
||||||
|
}
|
||||||
|
return f.Base.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirsMerger is how UnionFile weaves two directories together.
|
||||||
|
// It takes the FileInfo slices from the layer and the base and returns a
|
||||||
|
// single view.
|
||||||
|
type DirsMerger func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error)
|
||||||
|
|
||||||
|
var defaultUnionMergeDirsFn = func(lofi, bofi []os.FileInfo) ([]os.FileInfo, error) {
|
||||||
|
var files = make(map[string]os.FileInfo)
|
||||||
|
|
||||||
|
for _, fi := range lofi {
|
||||||
|
files[fi.Name()] = fi
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fi := range bofi {
|
||||||
|
if _, exists := files[fi.Name()]; !exists {
|
||||||
|
files[fi.Name()] = fi
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rfi := make([]os.FileInfo, len(files))
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for _, fi := range files {
|
||||||
|
rfi[i] = fi
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
return rfi, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readdir will weave the two directories together and
|
||||||
|
// return a single view of the overlayed directories
|
||||||
|
// At the end of the directory view, the error is io.EOF.
|
||||||
|
func (f *UnionFile) Readdir(c int) (ofi []os.FileInfo, err error) {
|
||||||
|
var merge DirsMerger = f.Merger
|
||||||
|
if merge == nil {
|
||||||
|
merge = defaultUnionMergeDirsFn
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.off == 0 {
|
||||||
|
var lfi []os.FileInfo
|
||||||
|
if f.Layer != nil {
|
||||||
|
lfi, err = f.Layer.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bfi []os.FileInfo
|
||||||
|
if f.Base != nil {
|
||||||
|
bfi, err = f.Base.Readdir(-1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
merged, err := merge(lfi, bfi)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
f.files = append(f.files, merged...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.off >= len(f.files) {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if c == -1 {
|
||||||
|
return f.files[f.off:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if c > len(f.files) {
|
||||||
|
c = len(f.files)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { f.off += c }()
|
||||||
|
return f.files[f.off:c], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Readdirnames(c int) ([]string, error) {
|
||||||
|
rfi, err := f.Readdir(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var names []string
|
||||||
|
for _, fi := range rfi {
|
||||||
|
names = append(names, fi.Name())
|
||||||
|
}
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Stat() (os.FileInfo, error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
return f.Layer.Stat()
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.Stat()
|
||||||
|
}
|
||||||
|
return nil, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Sync() (err error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
err = f.Layer.Sync()
|
||||||
|
if err == nil && f.Base != nil {
|
||||||
|
err = f.Base.Sync()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.Sync()
|
||||||
|
}
|
||||||
|
return BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) Truncate(s int64) (err error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
err = f.Layer.Truncate(s)
|
||||||
|
if err == nil && f.Base != nil {
|
||||||
|
err = f.Base.Truncate(s)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.Truncate(s)
|
||||||
|
}
|
||||||
|
return BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *UnionFile) WriteString(s string) (n int, err error) {
|
||||||
|
if f.Layer != nil {
|
||||||
|
n, err = f.Layer.WriteString(s)
|
||||||
|
if err == nil && f.Base != nil {
|
||||||
|
_, err = f.Base.WriteString(s)
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
if f.Base != nil {
|
||||||
|
return f.Base.WriteString(s)
|
||||||
|
}
|
||||||
|
return 0, BADFD
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyToLayer(base Fs, layer Fs, name string) error {
|
||||||
|
bfh, err := base.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer bfh.Close()
|
||||||
|
|
||||||
|
// First make sure the directory exists
|
||||||
|
exists, err := Exists(layer, filepath.Dir(name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
err = layer.MkdirAll(filepath.Dir(name), 0777) // FIXME?
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the file on the overlay
|
||||||
|
lfh, err := layer.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
n, err := io.Copy(lfh, bfh)
|
||||||
|
if err != nil {
|
||||||
|
// If anything fails, clean up the file
|
||||||
|
layer.Remove(name)
|
||||||
|
lfh.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
bfi, err := bfh.Stat()
|
||||||
|
if err != nil || bfi.Size() != n {
|
||||||
|
layer.Remove(name)
|
||||||
|
lfh.Close()
|
||||||
|
return syscall.EIO
|
||||||
|
}
|
||||||
|
|
||||||
|
err = lfh.Close()
|
||||||
|
if err != nil {
|
||||||
|
layer.Remove(name)
|
||||||
|
lfh.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return layer.Chtimes(name, bfi.ModTime(), bfi.ModTime())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,330 @@
|
||||||
|
// Copyright ©2015 Steve Francia <spf@spf13.com>
|
||||||
|
// Portions Copyright ©2015 The Hugo Authors
|
||||||
|
// Portions Copyright 2016-present Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package afero
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
|
"golang.org/x/text/transform"
|
||||||
|
"golang.org/x/text/unicode/norm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Filepath separator defined by os.Separator.
|
||||||
|
const FilePathSeparator = string(filepath.Separator)
|
||||||
|
|
||||||
|
// Takes a reader and a path and writes the content
|
||||||
|
func (a Afero) WriteReader(path string, r io.Reader) (err error) {
|
||||||
|
return WriteReader(a.Fs, path, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteReader(fs Fs, path string, r io.Reader) (err error) {
|
||||||
|
dir, _ := filepath.Split(path)
|
||||||
|
ospath := filepath.FromSlash(dir)
|
||||||
|
|
||||||
|
if ospath != "" {
|
||||||
|
err = fs.MkdirAll(ospath, 0777) // rwx, rw, r
|
||||||
|
if err != nil {
|
||||||
|
if err != os.ErrExist {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := fs.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as WriteReader but checks to see if file/directory already exists.
|
||||||
|
func (a Afero) SafeWriteReader(path string, r io.Reader) (err error) {
|
||||||
|
return SafeWriteReader(a.Fs, path, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SafeWriteReader(fs Fs, path string, r io.Reader) (err error) {
|
||||||
|
dir, _ := filepath.Split(path)
|
||||||
|
ospath := filepath.FromSlash(dir)
|
||||||
|
|
||||||
|
if ospath != "" {
|
||||||
|
err = fs.MkdirAll(ospath, 0777) // rwx, rw, r
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := Exists(fs, path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if exists {
|
||||||
|
return fmt.Errorf("%v already exists", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
file, err := fs.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(file, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) GetTempDir(subPath string) string {
|
||||||
|
return GetTempDir(a.Fs, subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTempDir returns the default temp directory with trailing slash
|
||||||
|
// if subPath is not empty then it will be created recursively with mode 777 rwx rwx rwx
|
||||||
|
func GetTempDir(fs Fs, subPath string) string {
|
||||||
|
addSlash := func(p string) string {
|
||||||
|
if FilePathSeparator != p[len(p)-1:] {
|
||||||
|
p = p + FilePathSeparator
|
||||||
|
}
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
dir := addSlash(os.TempDir())
|
||||||
|
|
||||||
|
if subPath != "" {
|
||||||
|
// preserve windows backslash :-(
|
||||||
|
if FilePathSeparator == "\\" {
|
||||||
|
subPath = strings.Replace(subPath, "\\", "____", -1)
|
||||||
|
}
|
||||||
|
dir = dir + UnicodeSanitize((subPath))
|
||||||
|
if FilePathSeparator == "\\" {
|
||||||
|
dir = strings.Replace(dir, "____", "\\", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists, _ := Exists(fs, dir); exists {
|
||||||
|
return addSlash(dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := fs.MkdirAll(dir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dir = addSlash(dir)
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rewrite string to remove non-standard path characters
|
||||||
|
func UnicodeSanitize(s string) string {
|
||||||
|
source := []rune(s)
|
||||||
|
target := make([]rune, 0, len(source))
|
||||||
|
|
||||||
|
for _, r := range source {
|
||||||
|
if unicode.IsLetter(r) ||
|
||||||
|
unicode.IsDigit(r) ||
|
||||||
|
unicode.IsMark(r) ||
|
||||||
|
r == '.' ||
|
||||||
|
r == '/' ||
|
||||||
|
r == '\\' ||
|
||||||
|
r == '_' ||
|
||||||
|
r == '-' ||
|
||||||
|
r == '%' ||
|
||||||
|
r == ' ' ||
|
||||||
|
r == '#' {
|
||||||
|
target = append(target, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(target)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform characters with accents into plain forms.
|
||||||
|
func NeuterAccents(s string) string {
|
||||||
|
t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
|
||||||
|
result, _, _ := transform.String(t, string(s))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func isMn(r rune) bool {
|
||||||
|
return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) FileContainsBytes(filename string, subslice []byte) (bool, error) {
|
||||||
|
return FileContainsBytes(a.Fs, filename, subslice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file contains a specified byte slice.
|
||||||
|
func FileContainsBytes(fs Fs, filename string, subslice []byte) (bool, error) {
|
||||||
|
f, err := fs.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return readerContainsAny(f, subslice), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) FileContainsAnyBytes(filename string, subslices [][]byte) (bool, error) {
|
||||||
|
return FileContainsAnyBytes(a.Fs, filename, subslices)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file contains any of the specified byte slices.
|
||||||
|
func FileContainsAnyBytes(fs Fs, filename string, subslices [][]byte) (bool, error) {
|
||||||
|
f, err := fs.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
return readerContainsAny(f, subslices...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// readerContains reports whether any of the subslices is within r.
|
||||||
|
func readerContainsAny(r io.Reader, subslices ...[]byte) bool {
|
||||||
|
|
||||||
|
if r == nil || len(subslices) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
largestSlice := 0
|
||||||
|
|
||||||
|
for _, sl := range subslices {
|
||||||
|
if len(sl) > largestSlice {
|
||||||
|
largestSlice = len(sl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if largestSlice == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
bufflen := largestSlice * 4
|
||||||
|
halflen := bufflen / 2
|
||||||
|
buff := make([]byte, bufflen)
|
||||||
|
var err error
|
||||||
|
var n, i int
|
||||||
|
|
||||||
|
for {
|
||||||
|
i++
|
||||||
|
if i == 1 {
|
||||||
|
n, err = io.ReadAtLeast(r, buff[:halflen], halflen)
|
||||||
|
} else {
|
||||||
|
if i != 2 {
|
||||||
|
// shift left to catch overlapping matches
|
||||||
|
copy(buff[:], buff[halflen:])
|
||||||
|
}
|
||||||
|
n, err = io.ReadAtLeast(r, buff[halflen:], halflen)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n > 0 {
|
||||||
|
for _, sl := range subslices {
|
||||||
|
if bytes.Contains(buff, sl) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) DirExists(path string) (bool, error) {
|
||||||
|
return DirExists(a.Fs, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DirExists checks if a path exists and is a directory.
|
||||||
|
func DirExists(fs Fs, path string) (bool, error) {
|
||||||
|
fi, err := fs.Stat(path)
|
||||||
|
if err == nil && fi.IsDir() {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) IsDir(path string) (bool, error) {
|
||||||
|
return IsDir(a.Fs, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDir checks if a given path is a directory.
|
||||||
|
func IsDir(fs Fs, path string) (bool, error) {
|
||||||
|
fi, err := fs.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return fi.IsDir(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) IsEmpty(path string) (bool, error) {
|
||||||
|
return IsEmpty(a.Fs, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty checks if a given file or directory is empty.
|
||||||
|
func IsEmpty(fs Fs, path string) (bool, error) {
|
||||||
|
if b, _ := Exists(fs, path); !b {
|
||||||
|
return false, fmt.Errorf("%q path does not exist", path)
|
||||||
|
}
|
||||||
|
fi, err := fs.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if fi.IsDir() {
|
||||||
|
f, err := fs.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
list, err := f.Readdir(-1)
|
||||||
|
return len(list) == 0, nil
|
||||||
|
}
|
||||||
|
return fi.Size() == 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a Afero) Exists(path string) (bool, error) {
|
||||||
|
return Exists(a.Fs, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a file or directory exists.
|
||||||
|
func Exists(fs Fs, path string) (bool, error) {
|
||||||
|
_, err := fs.Stat(path)
|
||||||
|
if err == nil {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func FullBaseFsPath(basePathFs *BasePathFs, relativePath string) string {
|
||||||
|
combinedPath := filepath.Join(basePathFs.path, relativePath)
|
||||||
|
if parent, ok := basePathFs.source.(*BasePathFs); ok {
|
||||||
|
return FullBaseFsPath(parent, combinedPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return combinedPath
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue