Skip to content

Commit 4e0d1af

Browse files
committed
storage: filesystem, Avoid overwriting loose obj files. Fixes go-git#55
Loose object files are content-addressable and imutable. They should be created on demand and deleted on repacking. However, they should not be overwritten - assuming the initial file isn't corrupted. The previous lack of validation meant those files were being overwritten when in fact they could just be ignored. In Linux, this was a non-issue, however, in Windows this operation led to Access Denied errors. Some additional moving parts of this fix: - [go-billy](go-git/go-billy#187): Align behaviour supporting dir.NewObject(): - Add support for Chmod in polyfill so that ChrootOS is able to chmod files. - Ensure temporary directories are created for BoundOS to avoid errors when trying to create the temporary file used for loose files. - This PR: - Ensure that in Windows, packed and loose object files are created as read-only, which in this case means setting the flag windows.FILE_ATTRIBUTE_READONLY via x/sys/windows. - Skip renaming the temporary file into the existing loose object, instead simply delete the temporary file. Relates to: - Southclaws/sampctl#422 - git-bug/git-bug#1142 - entireio/cli#455 Signed-off-by: Paulo Gomes <pjbgf@linux.com>
1 parent 394db9e commit 4e0d1af

7 files changed

Lines changed: 216 additions & 66 deletions

File tree

go.mod

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/emirpasic/gods v1.18.1
1111
github.com/gliderlabs/ssh v0.3.8
1212
github.com/go-git/gcfg/v2 v2.0.2
13-
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc
13+
github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f
1414
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67
1515
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8
1616
github.com/kevinburke/ssh_config v1.5.0
@@ -29,9 +29,6 @@ require (
2929
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
3030
github.com/davecgh/go-spew v1.1.1 // indirect
3131
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
32-
github.com/kr/pretty v0.3.1 // indirect
3332
github.com/pmezard/go-difflib v1.0.0 // indirect
34-
github.com/rogpeppe/go-internal v1.14.1 // indirect
35-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3633
gopkg.in/yaml.v3 v3.0.1 // indirect
3734
)

go.sum

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
88
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
99
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
1010
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
11-
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
1211
github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
1312
github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
1413
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -20,8 +19,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
2019
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
2120
github.com/go-git/gcfg/v2 v2.0.2 h1:MY5SIIfTGGEMhdA7d7JePuVVxtKL7Hp+ApGDJAJ7dpo=
2221
github.com/go-git/gcfg/v2 v2.0.2/go.mod h1:/lv2NsxvhepuMrldsFilrgct6pxzpGdSRC13ydTLSLs=
23-
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc h1:rhkjrnRkamkRC7woapp425E4CAH6RPcqsS9X8LA93IY=
24-
github.com/go-git/go-billy/v6 v6.0.0-20260114122816-19306b749ecc/go.mod h1:X1oe0Z2qMsa9hkar3AAPuL9hu4Mi3ztXEjdqRhr6fcc=
22+
github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f h1:Uvbx7nITO3Sd1GdXarX0TbyYmOaSNIJP0mm4LocEyyA=
23+
github.com/go-git/go-billy/v6 v6.0.0-20260226131633-45bd0956d66f/go.mod h1:ZW9JC5gionMP1kv5uiaOaV23q0FFmNrVOV8VW+y/acc=
2524
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67 h1:3hutPZF+/FBjR/9MdsLJ7e1mlt9pwHgwxMW7CrbmWII=
2625
github.com/go-git/go-git-fixtures/v5 v5.1.2-0.20260122163445-0622d7459a67/go.mod h1:xKt0pNHST9tYHvbiLxSY27CQWFwgIxBJuDrOE0JvbZw=
2726
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
@@ -30,22 +29,15 @@ github.com/kevinburke/ssh_config v1.5.0 h1:3cPZmE54xb5j3G5xQCjSvokqNwU2uW+3ry1+P
3029
github.com/kevinburke/ssh_config v1.5.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
3130
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
3231
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
32+
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
3333
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
34-
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
35-
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
36-
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
3734
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
35+
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
3836
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
39-
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
40-
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
4137
github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
4238
github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
43-
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
4439
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4540
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
46-
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
47-
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
48-
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
4941
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
5042
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
5143
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -63,9 +55,8 @@ golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
6355
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
6456
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
6557
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
58+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
6659
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
67-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
68-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
6960
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
7061
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
7162
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

storage/filesystem/dotgit/dotgit_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,3 +1086,61 @@ func (s *SuiteDotGit) TestSetPackedRef() {
10861086
s.Require().NoError(err)
10871087
s.Equal(1, looseCount)
10881088
}
1089+
1090+
func TestIssue55(t *testing.T) {
1091+
t.Parallel()
1092+
1093+
writeObject := func(fs billy.Filesystem) {
1094+
t.Helper()
1095+
1096+
dir := New(fs)
1097+
err := dir.Initialize()
1098+
require.NoError(t, err)
1099+
1100+
w, err := dir.NewObject()
1101+
require.NoError(t, err)
1102+
1103+
err = w.WriteHeader(plumbing.BlobObject, 14)
1104+
require.NoError(t, err)
1105+
n, err := w.Write([]byte("this is a test"))
1106+
require.NoError(t, err)
1107+
assert.Equal(t, 14, n)
1108+
1109+
assert.Equal(t, "a8a940627d132695a9769df883f85992f0ff4a43", w.Hash().String())
1110+
1111+
err = w.Close()
1112+
require.NoError(t, err)
1113+
}
1114+
1115+
for _, tc := range []struct {
1116+
name string
1117+
fs billy.Filesystem
1118+
}{
1119+
{"BoundOS", osfs.New(t.TempDir(), osfs.WithBoundOS())},
1120+
{"ChrootOS", osfs.New(t.TempDir(), osfs.WithChrootOS())},
1121+
} {
1122+
t.Run(tc.name, func(t *testing.T) {
1123+
t.Parallel()
1124+
path := filepath.Join("objects", "a8", "a940627d132695a9769df883f85992f0ff4a43")
1125+
1126+
writeObject(tc.fs)
1127+
i, err := tc.fs.Stat(path)
1128+
require.NoError(t, err)
1129+
assert.Equal(t, int64(34), i.Size())
1130+
1131+
ro, err := isReadOnly(tc.fs, path)
1132+
require.NoError(t, err)
1133+
assert.True(t, ro, "file %q is not read-only", path)
1134+
1135+
// Recreate the same object.
1136+
writeObject(tc.fs)
1137+
i, err = tc.fs.Stat(path)
1138+
require.NoError(t, err)
1139+
assert.Equal(t, int64(34), i.Size())
1140+
1141+
ro, err = isReadOnly(tc.fs, path)
1142+
require.NoError(t, err)
1143+
assert.True(t, ro, "file %q is not read-only", path)
1144+
})
1145+
}
1146+
}

storage/filesystem/dotgit/writers.go

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"fmt"
77
"hash"
88
"io"
9-
"runtime"
9+
"os"
1010
"sync/atomic"
1111

1212
"github.com/go-git/go-billy/v6"
@@ -17,7 +17,6 @@ import (
1717
"github.com/go-git/go-git/v6/plumbing/format/objfile"
1818
"github.com/go-git/go-git/v6/plumbing/format/packfile"
1919
"github.com/go-git/go-git/v6/plumbing/format/revfile"
20-
"github.com/go-git/go-git/v6/utils/trace"
2120
)
2221

2322
// PackWriter is a io.Writer that generates the packfile index simultaneously,
@@ -325,22 +324,17 @@ func (w *ObjectWriter) save() error {
325324
hex := h.String()
326325
file := w.fs.Join(objectsPath, hex[0:2], hex[2:h.HexSize()])
327326

327+
// Loose objects are content addressable, if they already exist
328+
// we can safely delete the temporary file and short-circuit the
329+
// operation.
330+
if _, err := w.fs.Stat(file); err == nil || os.IsExist(err) {
331+
return w.fs.Remove(w.f.Name())
332+
}
333+
328334
if err := w.fs.Rename(w.f.Name(), file); err != nil {
329335
return err
330336
}
331337
fixPermissions(w.fs, file)
332338

333339
return nil
334340
}
335-
336-
func fixPermissions(fs billy.Filesystem, path string) {
337-
if runtime.GOOS == "windows" {
338-
return
339-
}
340-
341-
if chmodFS, ok := fs.(billy.Chmod); ok {
342-
if err := chmodFS.Chmod(path, 0o444); err != nil {
343-
trace.General.Printf("failed to chmod %s: %v", path, err)
344-
}
345-
}
346-
}

storage/filesystem/dotgit/writers_test.go

Lines changed: 54 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import (
44
"fmt"
55
"io"
66
"os"
7+
"path/filepath"
78
"strconv"
89
"testing"
910

11+
"github.com/go-git/go-billy/v6"
1012
"github.com/go-git/go-billy/v6/osfs"
1113
"github.com/go-git/go-billy/v6/util"
1214
fixtures "github.com/go-git/go-git-fixtures/v5"
@@ -166,56 +168,74 @@ func TestPackWriterUnusedNotify(t *testing.T) {
166168
func TestPackWriterPermissions(t *testing.T) {
167169
t.Parallel()
168170

169-
f := fixtures.Basic().One()
171+
for _, tc := range []struct {
172+
name string
173+
fs billy.Filesystem
174+
}{
175+
{"BoundOS", osfs.New(t.TempDir(), osfs.WithBoundOS())},
176+
{"ChrootOS", osfs.New(t.TempDir(), osfs.WithChrootOS())},
177+
} {
178+
t.Run(tc.name, func(t *testing.T) {
179+
t.Parallel()
170180

171-
fs := osfs.New(t.TempDir(), osfs.WithBoundOS())
172-
dot := New(fs)
173-
require.NoError(t, dot.Initialize())
181+
f := fixtures.Basic().One()
174182

175-
w, err := dot.NewObjectPack()
176-
require.NoError(t, err)
183+
dot := New(tc.fs)
184+
require.NoError(t, dot.Initialize())
177185

178-
_, err = io.Copy(w, f.Packfile())
179-
require.NoError(t, err)
186+
w, err := dot.NewObjectPack()
187+
require.NoError(t, err)
180188

181-
require.NoError(t, w.Close())
189+
_, err = io.Copy(w, f.Packfile())
190+
require.NoError(t, err)
182191

183-
pfPath := fmt.Sprintf("objects/pack/pack-%s.pack", f.PackfileHash)
184-
idxPath := fmt.Sprintf("objects/pack/pack-%s.idx", f.PackfileHash)
185-
revPath := fmt.Sprintf("objects/pack/pack-%s.rev", f.PackfileHash)
192+
require.NoError(t, w.Close())
186193

187-
stat, err := fs.Stat(pfPath)
188-
require.NoError(t, err)
189-
assert.Equal(t, os.FileMode(0o444), stat.Mode().Perm())
194+
pfPath := filepath.Join("objects", "pack", fmt.Sprintf("pack-%s.pack", f.PackfileHash))
195+
idxPath := filepath.Join("objects", "pack", fmt.Sprintf("pack-%s.idx", f.PackfileHash))
190196

191-
stat, err = fs.Stat(idxPath)
192-
require.NoError(t, err)
193-
assert.Equal(t, os.FileMode(0o444), stat.Mode().Perm())
197+
ro, err := isReadOnly(tc.fs, pfPath)
198+
require.NoError(t, err)
199+
assert.True(t, ro, "file %q is not read-only", pfPath)
194200

195-
stat, err = fs.Stat(revPath)
196-
require.NoError(t, err)
197-
assert.Equal(t, os.FileMode(0o444), stat.Mode().Perm())
201+
ro, err = isReadOnly(tc.fs, idxPath)
202+
require.NoError(t, err)
203+
assert.True(t, ro, "file %q is not read-only", idxPath)
204+
})
205+
}
198206
}
199207

200208
func TestObjectWriterPermissions(t *testing.T) {
201209
t.Parallel()
202210

203-
fs := osfs.New(t.TempDir(), osfs.WithBoundOS())
204-
dot := New(fs)
205-
require.NoError(t, dot.Initialize())
211+
for _, tc := range []struct {
212+
name string
213+
fs billy.Filesystem
214+
}{
215+
{"BoundOS", osfs.New(t.TempDir(), osfs.WithBoundOS())},
216+
{"ChrootOS", osfs.New(t.TempDir(), osfs.WithChrootOS())},
217+
} {
218+
t.Run(tc.name, func(t *testing.T) {
219+
t.Parallel()
206220

207-
w, err := dot.NewObject()
208-
require.NoError(t, err)
221+
dot := New(tc.fs)
222+
require.NoError(t, dot.Initialize())
209223

210-
err = w.WriteHeader(plumbing.BlobObject, 14)
211-
require.NoError(t, err)
224+
w, err := dot.NewObject()
225+
require.NoError(t, err)
212226

213-
_, err = w.Write([]byte("this is a test"))
214-
require.NoError(t, err)
227+
err = w.WriteHeader(plumbing.BlobObject, 14)
228+
require.NoError(t, err)
215229

216-
require.NoError(t, w.Close())
230+
_, err = w.Write([]byte("this is a test"))
231+
require.NoError(t, err)
217232

218-
stat, err := fs.Stat("objects/a8/a940627d132695a9769df883f85992f0ff4a43")
219-
require.NoError(t, err)
220-
assert.Equal(t, os.FileMode(0o444), stat.Mode().Perm())
233+
require.NoError(t, w.Close())
234+
235+
path := filepath.Join("objects", "a8", "a940627d132695a9769df883f85992f0ff4a43")
236+
ro, err := isReadOnly(tc.fs, path)
237+
require.NoError(t, err)
238+
assert.True(t, ro, "file %q is not read-only", path)
239+
})
240+
}
221241
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//go:build !windows
2+
3+
package dotgit
4+
5+
import (
6+
"github.com/go-git/go-billy/v6"
7+
8+
"github.com/go-git/go-git/v6/utils/trace"
9+
)
10+
11+
const readOnly = 0o444
12+
13+
func fixPermissions(fs billy.Filesystem, path string) {
14+
if chmodFS, ok := fs.(billy.Chmod); ok {
15+
if err := chmodFS.Chmod(path, readOnly); err != nil {
16+
trace.General.Printf("failed to chmod %s: %v", path, err)
17+
}
18+
}
19+
}
20+
21+
func isReadOnly(fs billy.Filesystem, path string) (bool, error) {
22+
fi, err := fs.Stat(path)
23+
if err != nil {
24+
return false, err
25+
}
26+
27+
if fi.Mode().Perm() == readOnly {
28+
return true, nil
29+
}
30+
31+
return false, nil
32+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
//go:build windows
2+
3+
package dotgit
4+
5+
import (
6+
"fmt"
7+
"path/filepath"
8+
9+
"github.com/go-git/go-billy/v6"
10+
"golang.org/x/sys/windows"
11+
12+
"github.com/go-git/go-git/v6/utils/trace"
13+
)
14+
15+
func fixPermissions(fs billy.Filesystem, path string) {
16+
fullpath := filepath.Join(fs.Root(), path)
17+
p, err := windows.UTF16PtrFromString(fullpath)
18+
if err != nil {
19+
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
20+
return
21+
}
22+
23+
attrs, err := windows.GetFileAttributes(p)
24+
if err != nil {
25+
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
26+
return
27+
}
28+
29+
if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 {
30+
return
31+
}
32+
33+
err = windows.SetFileAttributes(p,
34+
attrs|windows.FILE_ATTRIBUTE_READONLY,
35+
)
36+
if err != nil {
37+
trace.General.Printf("failed to chmod %s: %v", fullpath, err)
38+
}
39+
}
40+
41+
func isReadOnly(fs billy.Filesystem, path string) (bool, error) {
42+
fullpath := filepath.Join(fs.Root(), path)
43+
p, err := windows.UTF16PtrFromString(fullpath)
44+
if err != nil {
45+
return false, fmt.Errorf("%w: %q", err, fullpath)
46+
}
47+
48+
attrs, err := windows.GetFileAttributes(p)
49+
if err != nil {
50+
return false, fmt.Errorf("%w: %q", err, fullpath)
51+
}
52+
53+
if attrs&windows.FILE_ATTRIBUTE_READONLY != 0 {
54+
return true, nil
55+
}
56+
57+
return false, nil
58+
}

0 commit comments

Comments
 (0)