From bd13001fb9520934bc57fce5639d8f2c4d13017a Mon Sep 17 00:00:00 2001 From: Enrico204 Date: Mon, 23 Aug 2021 11:36:09 +0200 Subject: [PATCH 1/2] Add health endpoint [draft] --- go.mod | 3 ++- go.sum | 19 +++++++++++++++ repo/repo.go | 40 +++++++++++++++++++++++++++++++ repo/repo_health_unix.go | 10 ++++++++ repo/repo_health_windows.go | 47 +++++++++++++++++++++++++++++++++++++ 5 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 repo/repo_health_unix.go create mode 100644 repo/repo_health_windows.go diff --git a/go.mod b/go.mod index c7476f5d..6b7ad858 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,14 @@ require ( github.com/felixge/httpsnoop v1.0.2 // indirect github.com/gorilla/handlers v1.5.1 github.com/minio/sha256-simd v1.0.0 + github.com/itchio/ox v0.0.0-20200826161350-12c6ca18d236 github.com/miolini/datacounter v1.0.2 github.com/prometheus/client_golang v1.11.0 github.com/prometheus/common v0.30.0 // indirect github.com/prometheus/procfs v0.7.2 // indirect github.com/spf13/cobra v1.2.1 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 - golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 // indirect + golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 google.golang.org/protobuf v1.27.1 // indirect ) diff --git a/go.sum b/go.sum index 4fe81d1a..eab8373d 100644 --- a/go.sum +++ b/go.sum @@ -69,8 +69,11 @@ github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pq github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/detailyang/go-fallocate v0.0.0-20180908115635-432fa640bd2e/go.mod h1:3ZQK6DMPSz/QZ73jlWxBtUhNA8xZx7LzUFSq/OfP8vk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -188,6 +191,10 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/itchio/headway v0.0.0-20191015112415-46f64dd4d524 h1:eROPirGQCZXnzRiwJw3bOE4OB5CctMF+zsnH91bmv9o= +github.com/itchio/headway v0.0.0-20191015112415-46f64dd4d524/go.mod h1:Iif+7HeesRB0PvTYf0gOIFX8lj0za0SUsWryENQYt1E= +github.com/itchio/ox v0.0.0-20200826161350-12c6ca18d236 h1:xRFytGxU9M4+0src0JjUf+1dn/gOBLL93SINzRS6M3I= +github.com/itchio/ox v0.0.0-20200826161350-12c6ca18d236/go.mod h1:WaIT8roTpS2W9zJKdHq4j+Zs5PcEY42H0FQZHQqEZFs= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -208,6 +215,8 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -232,12 +241,16 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -284,9 +297,11 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -439,6 +454,7 @@ golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200301153931-2f85c7ec1e52/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -644,6 +660,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -655,6 +673,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/repo/repo.go b/repo/repo.go index e27d56fa..5479e867 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -3,6 +3,7 @@ package repo import ( "encoding/hex" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -142,6 +143,24 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { httpMethodNotAllowed(w, []string{"HEAD", "GET", "POST", "DELETE"}) } return + } else if urlPath == "/health" { + switch r.Method { + case "HEAD": + fallthrough + case "GET": + err := h.RepoHealth() + if err != nil { + if h.opt.Debug { + log.Println("health check error: ", err) + } + w.WriteHeader(http.StatusInternalServerError) + } else { + w.WriteHeader(http.StatusOK) + } + return + default: + httpMethodNotAllowed(w, []string{"HEAD", "GET"}) + } } else if objectType, objectID := h.getObject(urlPath); objectType != "" { if objectID == "" { // TODO: add HEAD @@ -743,3 +762,24 @@ func (h *Handler) internalServerError(w http.ResponseWriter, err error) { } httpDefaultError(w, http.StatusInternalServerError) } + +// RepoHealth checks the repository health: writable path, free space, etc +// Returns an error when the health is not OK +func (h *Handler) RepoHealth() error { + if h.opt.Debug { + log.Println("RepoHealth()") + } + + // Check if the repo is writable [Linux/UNIX only code] + pathIsWritable, err := isWritable(h.path) + if err != nil { + return err + } else if !pathIsWritable { + return errors.New("Repository path is not writable") + } + + // Check if there is free space left + // TODO + + return nil +} diff --git a/repo/repo_health_unix.go b/repo/repo_health_unix.go new file mode 100644 index 00000000..39c9f384 --- /dev/null +++ b/repo/repo_health_unix.go @@ -0,0 +1,10 @@ +// +build !windows + +package repo + +import "golang.org/x/sys/unix" + +func isWritable(path string) (bool, error) { + err := unix.Access(path, unix.W_OK) + return err == nil, err +} diff --git a/repo/repo_health_windows.go b/repo/repo_health_windows.go new file mode 100644 index 00000000..89851430 --- /dev/null +++ b/repo/repo_health_windows.go @@ -0,0 +1,47 @@ +package repo + +import ( + "syscall" + + "github.com/itchio/ox/syscallex" + "github.com/itchio/ox/winox" +) + +var ( + advapi32DLL = syscall.NewLazyDLL("advapi32.dll") + impersonateSelfProc = advapi32DLL.NewProc("ImpersonateSelf") +) + +func ImpersonateSelf(impersonationLevel uint64) (bool, error) { + r1, _, err := impersonateSelfProc.Call( + uintptr(impersonationLevel), + ) + if err != syscall.Errno(0) { + return false, err + } + return syscall.Handle(r1) == 1, nil +} + +func isWritable(path string) (bool, error) { + if _, err := ImpersonateSelf(syscallex.SecurityImpersonation); err != nil { + return false, err + } + + defer func() { + _ = syscallex.RevertToSelf() + }() + + var impersonationToken syscall.Token + currentThread := syscallex.GetCurrentThread() + err := syscallex.OpenThreadToken( + currentThread, + syscall.TOKEN_ALL_ACCESS, + 1, + &impersonationToken, + ) + if err != nil { + return false, err + } + + return winox.UserHasPermission(impersonationToken, winox.RightsRead|winox.RightsWrite, path) +} From 3c1b00f1bc556c44929b9b187dce114cd60e1529 Mon Sep 17 00:00:00 2001 From: Enrico204 Date: Mon, 23 Aug 2021 12:24:39 +0200 Subject: [PATCH 2/2] Implement free space check --- repo/repo.go | 28 +++++++++++++++++++++++----- repo/repo_health_unix.go | 25 ++++++++++++++++++++++++- repo/repo_health_windows.go | 29 +++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/repo/repo.go b/repo/repo.go index 5479e867..60b81e87 100644 --- a/repo/repo.go +++ b/repo/repo.go @@ -151,9 +151,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { err := h.RepoHealth() if err != nil { if h.opt.Debug { - log.Println("health check error: ", err) + log.Println("health check error:", err) } - w.WriteHeader(http.StatusInternalServerError) + h.internalServerError(w, err) } else { w.WriteHeader(http.StatusOK) } @@ -773,13 +773,31 @@ func (h *Handler) RepoHealth() error { // Check if the repo is writable [Linux/UNIX only code] pathIsWritable, err := isWritable(h.path) if err != nil { + if h.opt.Debug { + log.Println("repository path check error:", err) + } return err } else if !pathIsWritable { - return errors.New("Repository path is not writable") + return errors.New("repository path is not writable") + } + if h.opt.Debug { + log.Println("repository path is writable") } - // Check if there is free space left - // TODO + // Check if there is some free space left + freeBytes, err := getFreeSpace(h.path) + if err != nil { + if h.opt.Debug { + log.Println("free space check error:", err) + } + return err + } + if h.opt.Debug { + log.Println("free space in bytes:", freeBytes) + } + if freeBytes < 8*1024*1024 { + return errors.New("free space is too low") + } return nil } diff --git a/repo/repo_health_unix.go b/repo/repo_health_unix.go index 39c9f384..b89b73df 100644 --- a/repo/repo_health_unix.go +++ b/repo/repo_health_unix.go @@ -1,10 +1,33 @@ +//go:build !windows // +build !windows package repo -import "golang.org/x/sys/unix" +import ( + "errors" + "io/fs" + "syscall" + + "golang.org/x/sys/unix" +) func isWritable(path string) (bool, error) { err := unix.Access(path, unix.W_OK) + var err2 syscall.Errno + if errors.As(err, &err2) && err2.Is(fs.ErrPermission) { + return false, nil + } return err == nil, err } + +func getFreeSpace(path string) (uint64, error) { + var stat unix.Statfs_t + + err := unix.Statfs(path, &stat) + if err != nil { + return 0, err + } + + // Available blocks * size per block = available space in bytes + return stat.Bavail * uint64(stat.Bsize), nil +} diff --git a/repo/repo_health_windows.go b/repo/repo_health_windows.go index 89851430..88246c88 100644 --- a/repo/repo_health_windows.go +++ b/repo/repo_health_windows.go @@ -1,15 +1,21 @@ package repo import ( + "errors" "syscall" + "unsafe" "github.com/itchio/ox/syscallex" "github.com/itchio/ox/winox" + "golang.org/x/sys/windows" ) var ( advapi32DLL = syscall.NewLazyDLL("advapi32.dll") impersonateSelfProc = advapi32DLL.NewProc("ImpersonateSelf") + + kernel32DLL = syscall.NewLazyDLL("kernel32.dll") + getDiskFreeSpaceExWProc = kernel32DLL.NewProc("GetDiskFreeSpaceExW") ) func ImpersonateSelf(impersonationLevel uint64) (bool, error) { @@ -22,6 +28,21 @@ func ImpersonateSelf(impersonationLevel uint64) (bool, error) { return syscall.Handle(r1) == 1, nil } +func GetDiskFreeSpaceExW(path string) (int64, error) { + var freeBytes int64 + + _, _, err := getDiskFreeSpaceExWProc.Call( + uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(path))), + uintptr(unsafe.Pointer(&freeBytes)), + 0, + 0, + ) + if err != syscall.Errno(0) { + return 0, err + } + return freeBytes, nil +} + func isWritable(path string) (bool, error) { if _, err := ImpersonateSelf(syscallex.SecurityImpersonation); err != nil { return false, err @@ -45,3 +66,11 @@ func isWritable(path string) (bool, error) { return winox.UserHasPermission(impersonationToken, winox.RightsRead|winox.RightsWrite, path) } + +func getFreeSpace(path string) (uint64, error) { + freeBytes, err := GetDiskFreeSpaceExW(path) + if err == nil && freeBytes < 0 { + return 0, errors.New("free space can't be negative") + } + return uint64(freeBytes), err +}