diff --git a/assets b/assets index d72688d..09a5168 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit d72688d7fceaa0bc916fc6637e928dfd5b5a1c43 +Subproject commit 09a5168b23349cb1d0201b544ba48ccb583886be diff --git a/go.mod b/go.mod index f243a6c..08478fe 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2 github.com/gin-gonic/gin v1.8.1 + github.com/glebarez/go-sqlite v1.20.3 github.com/go-ini/ini v1.50.0 github.com/go-mail/mail v2.3.1+incompatible github.com/go-playground/validator/v10 v10.11.0 @@ -59,7 +60,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3 // indirect github.com/dsnet/compress v0.0.1 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d // indirect github.com/envoyproxy/protoc-gen-validate v0.6.1 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect @@ -79,7 +80,7 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/certificate-transparency-go v1.1.2-0.20210511102531-373a877eec92 // indirect - github.com/google/go-cmp v0.5.5 // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect @@ -98,9 +99,8 @@ require ( github.com/leodido/go-urn v1.2.1 // indirect github.com/lib/pq v1.10.3 // indirect github.com/mattn/go-colorable v0.1.4 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.12 // indirect - github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -116,6 +116,7 @@ require ( github.com/prometheus/common v0.24.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect github.com/rivo/uniseg v0.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/satori/go.uuid v1.2.0 // indirect @@ -149,7 +150,7 @@ require ( golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect golang.org/x/oauth2 v0.0.0-20210427180440-81ed05c6b58c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20220702020025-31831981b65f // indirect + golang.org/x/sys v0.4.0 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/tools v0.1.0 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect @@ -162,6 +163,10 @@ require ( gopkg.in/mail.v2 v2.3.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.22.2 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.20.3 // indirect sigs.k8s.io/yaml v1.2.0 // indirect ) diff --git a/go.sum b/go.sum index 2354458..4d35a94 100644 --- a/go.sum +++ b/go.sum @@ -211,8 +211,9 @@ github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdf github.com/duo-labs/webauthn v0.0.0-20220330035159-03696f3d4499 h1:jaQHuGKk9NVcfu9VbA7ygslr/7utxdYs47i4osBhZP8= github.com/duo-labs/webauthn v0.0.0-20220330035159-03696f3d4499/go.mod h1:UMk1JMDgQDcdI2vQz+WJOIUTSjIq07qSepAVgc93rUc= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -269,6 +270,8 @@ github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/ github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= +github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -381,8 +384,9 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= github.com/google/go-licenses v0.0.0-20210329231322-ce1d9163b77d/go.mod h1:+TYOmkVoJOpwnS0wfdsJCV9CoD5nJYsHoFk/0CrTK4M= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= @@ -406,6 +410,7 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg= github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= @@ -590,8 +595,8 @@ github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -602,7 +607,6 @@ github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsO github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v1.14.7/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -751,6 +755,9 @@ github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTep github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1 h1:leEwA4MD1ew0lNgzz6Q4G76G3AEfeci+TMggN6WuFRs= github.com/rafaeljusto/redigomock v0.0.0-20191117212112-00b2509252a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 h1:VstopitMQi3hZP0fzvnsLmzXZdQGc4bEcgu24cp+d4M= +github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -1160,8 +1167,9 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f h1:xdsejrW/0Wf2diT5CPp3XmKUNbr7Xvw8kYilQ+6qjRY= -golang.org/x/sys v0.0.0-20220702020025-31831981b65f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1434,6 +1442,14 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ= honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= +modernc.org/libc v1.22.2 h1:4U7v51GyhlWqQmwCHj28Rdq2Yzwk55ovjFrdPjs8Hb0= +modernc.org/libc v1.22.2/go.mod h1:uvQavJ1pZ0hIoC/jfqNoMLURIMhKzINIWypNM17puug= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.20.3 h1:SqGJMMxjj1PHusLxdYxeQSodg7Jxn9WWkaAQjKrntZs= +modernc.org/sqlite v1.20.3/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= pack.ag/amqp v0.11.2/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= diff --git a/models/dialect_sqlite.go b/models/dialect_sqlite.go new file mode 100644 index 0000000..8368b69 --- /dev/null +++ b/models/dialect_sqlite.go @@ -0,0 +1,288 @@ +package model + +import ( + "fmt" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/jinzhu/gorm" +) + +var keyNameRegex = regexp.MustCompile("[^a-zA-Z0-9]+") + +// DefaultForeignKeyNamer contains the default foreign key name generator method +type DefaultForeignKeyNamer struct { +} + +type commonDialect struct { + db gorm.SQLCommon + DefaultForeignKeyNamer +} + +func (commonDialect) GetName() string { + return "common" +} + +func (s *commonDialect) SetDB(db gorm.SQLCommon) { + s.db = db +} + +func (commonDialect) BindVar(i int) string { + return "$$$" // ? +} + +func (commonDialect) Quote(key string) string { + return fmt.Sprintf(`"%s"`, key) +} + +func (s *commonDialect) fieldCanAutoIncrement(field *gorm.StructField) bool { + if value, ok := field.TagSettingsGet("AUTO_INCREMENT"); ok { + return strings.ToLower(value) != "false" + } + return field.IsPrimaryKey +} + +func (s *commonDialect) DataTypeOf(field *gorm.StructField) string { + var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field, s) + + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "BOOLEAN" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if s.fieldCanAutoIncrement(field) { + sqlType = "INTEGER AUTO_INCREMENT" + } else { + sqlType = "INTEGER" + } + case reflect.Int64, reflect.Uint64: + if s.fieldCanAutoIncrement(field) { + sqlType = "BIGINT AUTO_INCREMENT" + } else { + sqlType = "BIGINT" + } + case reflect.Float32, reflect.Float64: + sqlType = "FLOAT" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("VARCHAR(%d)", size) + } else { + sqlType = "VARCHAR(65532)" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "TIMESTAMP" + } + default: + if _, ok := dataValue.Interface().([]byte); ok { + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("BINARY(%d)", size) + } else { + sqlType = "BINARY(65532)" + } + } + } + } + + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for commonDialect", dataValue.Type().Name(), dataValue.Kind().String())) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) +} + +func currentDatabaseAndTable(dialect gorm.Dialect, tableName string) (string, string) { + if strings.Contains(tableName, ".") { + splitStrings := strings.SplitN(tableName, ".", 2) + return splitStrings[0], splitStrings[1] + } + return dialect.CurrentDatabase(), tableName +} + +func (s commonDialect) HasIndex(tableName string, indexName string) bool { + var count int + currentDatabase, tableName := currentDatabaseAndTable(&s, tableName) + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.STATISTICS WHERE table_schema = ? AND table_name = ? AND index_name = ?", currentDatabase, tableName, indexName).Scan(&count) + return count > 0 +} + +func (s commonDialect) RemoveIndex(tableName string, indexName string) error { + _, err := s.db.Exec(fmt.Sprintf("DROP INDEX %v", indexName)) + return err +} + +func (s commonDialect) HasForeignKey(tableName string, foreignKeyName string) bool { + return false +} + +func (s commonDialect) HasTable(tableName string) bool { + var count int + currentDatabase, tableName := currentDatabaseAndTable(&s, tableName) + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = ? AND table_name = ?", currentDatabase, tableName).Scan(&count) + return count > 0 +} + +func (s commonDialect) HasColumn(tableName string, columnName string) bool { + var count int + currentDatabase, tableName := currentDatabaseAndTable(&s, tableName) + s.db.QueryRow("SELECT count(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE table_schema = ? AND table_name = ? AND column_name = ?", currentDatabase, tableName, columnName).Scan(&count) + return count > 0 +} + +func (s commonDialect) ModifyColumn(tableName string, columnName string, typ string) error { + _, err := s.db.Exec(fmt.Sprintf("ALTER TABLE %v ALTER COLUMN %v TYPE %v", tableName, columnName, typ)) + return err +} + +func (s commonDialect) CurrentDatabase() (name string) { + s.db.QueryRow("SELECT DATABASE()").Scan(&name) + return +} + +func (commonDialect) LimitAndOffsetSQL(limit, offset interface{}) (sql string) { + if limit != nil { + if parsedLimit, err := strconv.ParseInt(fmt.Sprint(limit), 0, 0); err == nil && parsedLimit >= 0 { + sql += fmt.Sprintf(" LIMIT %d", parsedLimit) + } + } + if offset != nil { + if parsedOffset, err := strconv.ParseInt(fmt.Sprint(offset), 0, 0); err == nil && parsedOffset >= 0 { + sql += fmt.Sprintf(" OFFSET %d", parsedOffset) + } + } + return +} + +func (commonDialect) SelectFromDummyTable() string { + return "" +} + +func (commonDialect) LastInsertIDReturningSuffix(tableName, columnName string) string { + return "" +} + +func (commonDialect) DefaultValueStr() string { + return "DEFAULT VALUES" +} + +// BuildKeyName returns a valid key name (foreign key, index key) for the given table, field and reference +func (DefaultForeignKeyNamer) BuildKeyName(kind, tableName string, fields ...string) string { + keyName := fmt.Sprintf("%s_%s_%s", kind, tableName, strings.Join(fields, "_")) + keyName = keyNameRegex.ReplaceAllString(keyName, "_") + return keyName +} + +// NormalizeIndexAndColumn returns argument's index name and column name without doing anything +func (commonDialect) NormalizeIndexAndColumn(indexName, columnName string) (string, string) { + return indexName, columnName +} + +// IsByteArrayOrSlice returns true of the reflected value is an array or slice +func IsByteArrayOrSlice(value reflect.Value) bool { + return (value.Kind() == reflect.Array || value.Kind() == reflect.Slice) && value.Type().Elem() == reflect.TypeOf(uint8(0)) +} + +type sqlite struct { + commonDialect +} + +func init() { + gorm.RegisterDialect("sqlite", &sqlite{}) +} + +func (sqlite) GetName() string { + return "sqlite" +} + +// Get Data Type for Sqlite Dialect +func (s *sqlite) DataTypeOf(field *gorm.StructField) string { + var dataValue, sqlType, size, additionalType = gorm.ParseFieldStructForDialect(field, s) + + if sqlType == "" { + switch dataValue.Kind() { + case reflect.Bool: + sqlType = "bool" + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uintptr: + if s.fieldCanAutoIncrement(field) { + field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT") + sqlType = "integer primary key autoincrement" + } else { + sqlType = "integer" + } + case reflect.Int64, reflect.Uint64: + if s.fieldCanAutoIncrement(field) { + field.TagSettingsSet("AUTO_INCREMENT", "AUTO_INCREMENT") + sqlType = "integer primary key autoincrement" + } else { + sqlType = "bigint" + } + case reflect.Float32, reflect.Float64: + sqlType = "real" + case reflect.String: + if size > 0 && size < 65532 { + sqlType = fmt.Sprintf("varchar(%d)", size) + } else { + sqlType = "text" + } + case reflect.Struct: + if _, ok := dataValue.Interface().(time.Time); ok { + sqlType = "datetime" + } + default: + if IsByteArrayOrSlice(dataValue) { + sqlType = "blob" + } + } + } + + if sqlType == "" { + panic(fmt.Sprintf("invalid sql type %s (%s) for sqlite", dataValue.Type().Name(), dataValue.Kind().String())) + } + + if strings.TrimSpace(additionalType) == "" { + return sqlType + } + return fmt.Sprintf("%v %v", sqlType, additionalType) +} + +func (s sqlite) HasIndex(tableName string, indexName string) bool { + var count int + s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND sql LIKE '%%INDEX %v ON%%'", indexName), tableName).Scan(&count) + return count > 0 +} + +func (s sqlite) HasTable(tableName string) bool { + var count int + s.db.QueryRow("SELECT count(*) FROM sqlite_master WHERE type='table' AND name=?", tableName).Scan(&count) + return count > 0 +} + +func (s sqlite) HasColumn(tableName string, columnName string) bool { + var count int + s.db.QueryRow(fmt.Sprintf("SELECT count(*) FROM sqlite_master WHERE tbl_name = ? AND (sql LIKE '%%\"%v\" %%' OR sql LIKE '%%%v %%');\n", columnName, columnName), tableName).Scan(&count) + return count > 0 +} + +func (s sqlite) CurrentDatabase() (name string) { + var ( + ifaces = make([]interface{}, 3) + pointers = make([]*string, 3) + i int + ) + for i = 0; i < 3; i++ { + ifaces[i] = &pointers[i] + } + if err := s.db.QueryRow("PRAGMA database_list").Scan(ifaces...); err != nil { + return + } + if pointers[1] != nil { + name = *pointers[1] + } + return +} diff --git a/models/file.go b/models/file.go index 161bbbb..8a7b3c8 100644 --- a/models/file.go +++ b/models/file.go @@ -234,7 +234,7 @@ func DeleteFiles(files []*File, uid uint) error { user.ID = uid var size uint64 for _, file := range files { - if file.UserID != uid { + if uid > 0 && file.UserID != uid { tx.Rollback() return errors.New("user id not consistent") } @@ -253,9 +253,11 @@ func DeleteFiles(files []*File, uid uint) error { size += file.Size } - if err := user.ChangeStorage(tx, "-", size); err != nil { - tx.Rollback() - return err + if uid > 0 { + if err := user.ChangeStorage(tx, "-", size); err != nil { + tx.Rollback() + return err + } } return tx.Commit().Error diff --git a/models/file_test.go b/models/file_test.go index 5f6826c..6a18151 100644 --- a/models/file_test.go +++ b/models/file_test.go @@ -365,7 +365,7 @@ func TestDeleteFiles(t *testing.T) { // uid 不一致 { - err := DeleteFiles([]*File{{}}, 1) + err := DeleteFiles([]*File{{UserID: 2}}, 1) a.Contains("user id not consistent", err.Error()) } @@ -375,7 +375,7 @@ func TestDeleteFiles(t *testing.T) { mock.ExpectExec("DELETE(.+)"). WillReturnError(errors.New("error")) mock.ExpectRollback() - err := DeleteFiles([]*File{{}}, 0) + err := DeleteFiles([]*File{{UserID: 1}}, 1) a.NoError(mock.ExpectationsWereMet()) a.Error(err) } @@ -387,7 +387,7 @@ func TestDeleteFiles(t *testing.T) { WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("UPDATE(.+)storage(.+)").WillReturnError(errors.New("error")) mock.ExpectRollback() - err := DeleteFiles([]*File{{}}, 0) + err := DeleteFiles([]*File{{UserID: 1}}, 1) a.NoError(mock.ExpectationsWereMet()) a.Error(err) } @@ -398,7 +398,7 @@ func TestDeleteFiles(t *testing.T) { mock.ExpectExec("DELETE(.+)"). WillReturnResult(sqlmock.NewResult(1, 0)) mock.ExpectRollback() - err := DeleteFiles([]*File{{Size: 1}, {Size: 2}}, 0) + err := DeleteFiles([]*File{{Size: 1, UserID: 1}, {Size: 2, UserID: 1}}, 1) a.NoError(mock.ExpectationsWereMet()) a.Error(err) a.Contains("file size is dirty", err.Error()) @@ -411,9 +411,22 @@ func TestDeleteFiles(t *testing.T) { WillReturnResult(sqlmock.NewResult(2, 1)) mock.ExpectExec("DELETE(.+)"). WillReturnResult(sqlmock.NewResult(2, 1)) - mock.ExpectExec("UPDATE(.+)storage(.+)").WithArgs(uint64(3), sqlmock.AnyArg()).WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectExec("UPDATE(.+)storage(.+)").WithArgs(uint64(3), sqlmock.AnyArg(), uint(1)).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() - err := DeleteFiles([]*File{{Size: 1}, {Size: 2}}, 0) + err := DeleteFiles([]*File{{Size: 1, UserID: 1}, {Size: 2, UserID: 1}}, 1) + a.NoError(mock.ExpectationsWereMet()) + a.NoError(err) + } + + // 成功, 关联用户不存在 + { + mock.ExpectBegin() + mock.ExpectExec("DELETE(.+)"). + WillReturnResult(sqlmock.NewResult(2, 1)) + mock.ExpectExec("DELETE(.+)"). + WillReturnResult(sqlmock.NewResult(2, 1)) + mock.ExpectCommit() + err := DeleteFiles([]*File{{Size: 1, UserID: 1}, {Size: 2, UserID: 1}}, 0) a.NoError(mock.ExpectationsWereMet()) a.NoError(err) } diff --git a/models/folder_test.go b/models/folder_test.go index 2358eec..90220ca 100644 --- a/models/folder_test.go +++ b/models/folder_test.go @@ -106,7 +106,7 @@ func TestFolder_GetChildFolder(t *testing.T) { } func TestGetRecursiveChildFolderSQLite(t *testing.T) { - conf.DatabaseConfig.Type = "sqlite3" + conf.DatabaseConfig.Type = "sqlite" asserts := assert.New(t) // 测试目录结构 diff --git a/models/group.go b/models/group.go index 490fc38..7b8930c 100644 --- a/models/group.go +++ b/models/group.go @@ -34,6 +34,7 @@ type GroupOption struct { SourceBatchSize int `json:"source_batch,omitempty"` RedirectedSource bool `json:"redirected_source,omitempty"` Aria2BatchSize int `json:"aria2_batch,omitempty"` + AdvanceDelete bool `json:"advance_delete,omitempty"` } // GetGroupByID 用ID获取用户组 diff --git a/models/init.go b/models/init.go index 0ffde92..16c0fce 100644 --- a/models/init.go +++ b/models/init.go @@ -9,10 +9,10 @@ import ( "github.com/gin-gonic/gin" "github.com/jinzhu/gorm" + _ "github.com/glebarez/go-sqlite" _ "github.com/jinzhu/gorm/dialects/mssql" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/postgres" - _ "github.com/jinzhu/gorm/dialects/sqlite" ) // DB 数据库链接单例 @@ -23,20 +23,26 @@ func Init() { util.Log().Info("Initializing database connection...") var ( - db *gorm.DB - err error + db *gorm.DB + err error + confDBType string = conf.DatabaseConfig.Type ) + // 兼容已有配置中的 "sqlite3" 配置项 + if confDBType == "sqlite3" { + confDBType = "sqlite" + } + if gin.Mode() == gin.TestMode { // 测试模式下,使用内存数据库 - db, err = gorm.Open("sqlite3", ":memory:") + db, err = gorm.Open("sqlite", ":memory:") } else { - switch conf.DatabaseConfig.Type { - case "UNSET", "sqlite", "sqlite3": - // 未指定数据库或者明确指定为 sqlite 时,使用 SQLite3 数据库 - db, err = gorm.Open("sqlite3", util.RelativePath(conf.DatabaseConfig.DBFile)) + switch confDBType { + case "UNSET", "sqlite": + // 未指定数据库或者明确指定为 sqlite 时,使用 SQLite 数据库 + db, err = gorm.Open("sqlite", util.RelativePath(conf.DatabaseConfig.DBFile)) case "postgres": - db, err = gorm.Open(conf.DatabaseConfig.Type, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", + db, err = gorm.Open(confDBType, fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable", conf.DatabaseConfig.Host, conf.DatabaseConfig.User, conf.DatabaseConfig.Password, @@ -53,14 +59,14 @@ func Init() { conf.DatabaseConfig.Port) } - db, err = gorm.Open(conf.DatabaseConfig.Type, fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local", + db, err = gorm.Open(confDBType, fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=True&loc=Local", conf.DatabaseConfig.User, conf.DatabaseConfig.Password, host, conf.DatabaseConfig.Name, conf.DatabaseConfig.Charset)) default: - util.Log().Panic("Unsupported database type %q.", conf.DatabaseConfig.Type) + util.Log().Panic("Unsupported database type %q.", confDBType) } } @@ -83,7 +89,7 @@ func Init() { //设置连接池 db.DB().SetMaxIdleConns(50) - if conf.DatabaseConfig.Type == "sqlite" || conf.DatabaseConfig.Type == "sqlite3" || conf.DatabaseConfig.Type == "UNSET" { + if confDBType == "sqlite" || confDBType == "UNSET" { db.DB().SetMaxOpenConns(1) } else { db.DB().SetMaxOpenConns(100) diff --git a/models/migration.go b/models/migration.go index 17a08ce..fad6a76 100644 --- a/models/migration.go +++ b/models/migration.go @@ -111,6 +111,7 @@ func addDefaultGroups() { SourceBatchSize: 1000, Aria2BatchSize: 50, RedirectedSource: true, + AdvanceDelete: true, }, } if err := DB.Create(&defaultAdminGroup).Error; err != nil { diff --git a/models/migration_test.go b/models/migration_test.go index 55faefe..7c9d673 100644 --- a/models/migration_test.go +++ b/models/migration_test.go @@ -10,8 +10,8 @@ import ( func TestMigration(t *testing.T) { asserts := assert.New(t) - conf.DatabaseConfig.Type = "sqlite3" - DB, _ = gorm.Open("sqlite3", ":memory:") + conf.DatabaseConfig.Type = "sqlite" + DB, _ = gorm.Open("sqlite", ":memory:") asserts.NotPanics(func() { migration() diff --git a/models/webdav.go b/models/webdav.go index c1492f3..fef4495 100644 --- a/models/webdav.go +++ b/models/webdav.go @@ -11,6 +11,7 @@ type Webdav struct { Password string `gorm:"unique_index:password_only_on"` // 应用密码 UserID uint `gorm:"unique_index:password_only_on"` // 用户ID Root string `gorm:"type:text"` // 根目录 + Readonly bool `gorm:"type:bool"` // 是否只读 } // Create 创建账户 @@ -39,3 +40,8 @@ func ListWebDAVAccounts(uid uint) []Webdav { func DeleteWebDAVAccountByID(id, uid uint) { DB.Where("user_id = ? and id = ?", uid, id).Delete(&Webdav{}) } + +// UpdateWebDAVAccountReadonlyByID 根据账户ID和UID更新账户的只读性 +func UpdateWebDAVAccountReadonlyByID(id, uid uint, readonly bool) { + DB.Model(&Webdav{Model: gorm.Model{ID: id}, UserID: uid}).UpdateColumn("readonly", readonly) +} diff --git a/pkg/crontab/collect.go b/pkg/crontab/collect.go index 0667570..a5678f6 100644 --- a/pkg/crontab/collect.go +++ b/pkg/crontab/collect.go @@ -88,7 +88,7 @@ func uploadSessionCollect() { continue } - if err = fs.Delete(context.Background(), []uint{}, filesIDs, false); err != nil { + if err = fs.Delete(context.Background(), []uint{}, filesIDs, false, false); err != nil { util.Log().Warning("Failed to delete upload session: %s", err) } diff --git a/pkg/filesystem/manage.go b/pkg/filesystem/manage.go index 34c54c7..670f4cd 100644 --- a/pkg/filesystem/manage.go +++ b/pkg/filesystem/manage.go @@ -120,8 +120,9 @@ func (fs *FileSystem) Move(ctx context.Context, dirs, files []uint, src, dst str return err } -// Delete 递归删除对象, force 为 true 时强制删除文件记录,忽略物理删除是否成功 -func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint, force bool) error { +// Delete 递归删除对象, force 为 true 时强制删除文件记录,忽略物理删除是否成功; +// unlink 为 true 时只删除虚拟文件系统的文件记录,不删除物理文件。 +func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint, force, unlink bool) error { // 已删除的文件ID var deletedFiles = make([]*model.File, 0, len(fs.FileTarget)) // 删除失败的文件的父目录ID @@ -155,7 +156,10 @@ func (fs *FileSystem) Delete(ctx context.Context, dirs, files []uint, force bool policyGroup := fs.GroupFileByPolicy(ctx, filesToBeDelete) // 按照存储策略分组删除对象 - failed := fs.deleteGroupedFile(ctx, policyGroup) + failed := make(map[uint][]string) + if !unlink { + failed = fs.deleteGroupedFile(ctx, policyGroup) + } // 整理删除结果 for i := 0; i < len(fs.FileTarget); i++ { diff --git a/pkg/filesystem/manage_test.go b/pkg/filesystem/manage_test.go index 2ec0aec..a239831 100644 --- a/pkg/filesystem/manage_test.go +++ b/pkg/filesystem/manage_test.go @@ -3,13 +3,13 @@ package filesystem import ( "context" "errors" + "github.com/DATA-DOG/go-sqlmock" "os" "testing" "github.com/cloudreve/Cloudreve/v3/pkg/filesystem/response" testMock "github.com/stretchr/testify/mock" - "github.com/DATA-DOG/go-sqlmock" model "github.com/cloudreve/Cloudreve/v3/models" "github.com/cloudreve/Cloudreve/v3/pkg/cache" "github.com/cloudreve/Cloudreve/v3/pkg/conf" @@ -485,7 +485,6 @@ func TestFileSystem_Delete(t *testing.T) { WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("DELETE(.+)"). WillReturnResult(sqlmock.NewResult(0, 1)) - mock.ExpectExec("UPDATE(.+)users(.+)storage(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() // 删除对应分享 mock.ExpectBegin() @@ -505,7 +504,7 @@ func TestFileSystem_Delete(t *testing.T) { fs.FileTarget = []model.File{} fs.DirTarget = []model.Folder{} - err := fs.Delete(ctx, []uint{1}, []uint{1}, true) + err := fs.Delete(ctx, []uint{1}, []uint{1}, true, false) asserts.NoError(err) } //全部成功 @@ -543,7 +542,6 @@ func TestFileSystem_Delete(t *testing.T) { WillReturnResult(sqlmock.NewResult(0, 1)) mock.ExpectExec("DELETE(.+)"). WillReturnResult(sqlmock.NewResult(0, 1)) - mock.ExpectExec("UPDATE(.+)users(.+)storage(.+)").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() // 删除对应分享 mock.ExpectBegin() @@ -563,7 +561,7 @@ func TestFileSystem_Delete(t *testing.T) { fs.FileTarget = []model.File{} fs.DirTarget = []model.Folder{} - err = fs.Delete(ctx, []uint{1}, []uint{1}, false) + err = fs.Delete(ctx, []uint{1}, []uint{1}, false, false) asserts.NoError(err) } diff --git a/pkg/serializer/user.go b/pkg/serializer/user.go index 68e9940..45b703e 100644 --- a/pkg/serializer/user.go +++ b/pkg/serializer/user.go @@ -41,6 +41,7 @@ type group struct { CompressEnabled bool `json:"compress"` WebDAVEnabled bool `json:"webdav"` SourceBatchSize int `json:"sourceBatch"` + AdvanceDelete bool `json:"advanceDelete"` } type tag struct { @@ -100,6 +101,7 @@ func BuildUser(user model.User) User { CompressEnabled: user.Group.OptionsSerialized.ArchiveTask, WebDAVEnabled: user.Group.WebDAVEnabled, SourceBatchSize: user.Group.OptionsSerialized.SourceBatchSize, + AdvanceDelete: user.Group.OptionsSerialized.AdvanceDelete, }, Tags: buildTagRes(tags), } diff --git a/pkg/util/common_test.go b/pkg/util/common_test.go index f7d1ff8..ae4c47f 100644 --- a/pkg/util/common_test.go +++ b/pkg/util/common_test.go @@ -66,7 +66,7 @@ func TestBuildRegexp(t *testing.T) { func TestBuildConcat(t *testing.T) { asserts := assert.New(t) asserts.Equal("CONCAT(1,2)", BuildConcat("1", "2", "mysql")) - asserts.Equal("1||2", BuildConcat("1", "2", "sqlite3")) + asserts.Equal("1||2", BuildConcat("1", "2", "sqlite")) } func TestSliceDifference(t *testing.T) { diff --git a/pkg/webdav/webdav.go b/pkg/webdav/webdav.go index 208bd12..8ee02cf 100644 --- a/pkg/webdav/webdav.go +++ b/pkg/webdav/webdav.go @@ -218,7 +218,7 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string, fs *filesystem. }, 0, nil } -//OK +// OK func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request, fs *filesystem.FileSystem) (status int, err error) { reqPath, status, err := h.stripPrefix(r.URL.Path, fs.User.ID) if err != nil { @@ -303,7 +303,7 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, fs *files // 尝试作为文件删除 if ok, file := fs.IsFileExist(reqPath); ok { - if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false); err != nil { + if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil { return http.StatusMethodNotAllowed, err } return http.StatusNoContent, nil @@ -311,7 +311,7 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request, fs *files // 尝试作为目录删除 if ok, folder := fs.IsPathExist(reqPath); ok { - if err := fs.Delete(ctx, []uint{folder.ID}, []uint{}, false); err != nil { + if err := fs.Delete(ctx, []uint{folder.ID}, []uint{}, false, false); err != nil { return http.StatusMethodNotAllowed, err } return http.StatusNoContent, nil @@ -783,10 +783,11 @@ const ( // infiniteDepth. Parsing any other string returns invalidDepth. // // Different WebDAV methods have further constraints on valid depths: -// - PROPFIND has no further restrictions, as per section 9.1. -// - COPY accepts only "0" or "infinity", as per section 9.8.3. -// - MOVE accepts only "infinity", as per section 9.9.2. -// - LOCK accepts only "0" or "infinity", as per section 9.10.3. +// - PROPFIND has no further restrictions, as per section 9.1. +// - COPY accepts only "0" or "infinity", as per section 9.8.3. +// - MOVE accepts only "infinity", as per section 9.9.2. +// - LOCK accepts only "0" or "infinity", as per section 9.10.3. +// // These constraints are enforced by the handleXxx methods. func parseDepth(s string) int { switch s { diff --git a/routers/controllers/webdav.go b/routers/controllers/webdav.go index d44cd18..5f3f084 100644 --- a/routers/controllers/webdav.go +++ b/routers/controllers/webdav.go @@ -7,6 +7,7 @@ import ( "github.com/cloudreve/Cloudreve/v3/pkg/webdav" "github.com/cloudreve/Cloudreve/v3/service/setting" "github.com/gin-gonic/gin" + "net/http" "sync" ) @@ -39,6 +40,15 @@ func ServeWebDAV(c *gin.Context) { fs.Root = root } } + + // 检查是否只读 + if application.Readonly { + switch c.Request.Method { + case "DELETE", "PUT", "MKCOL", "COPY", "MOVE": + c.Status(http.StatusForbidden) + return + } + } } handler.ServeHTTP(c.Writer, c.Request, fs) @@ -66,6 +76,17 @@ func DeleteWebDAVAccounts(c *gin.Context) { } } +// UpdateWebDAVAccountsReadonly 更改WebDAV账户只读性 +func UpdateWebDAVAccountsReadonly(c *gin.Context) { + var service setting.WebDAVAccountUpdateReadonlyService + if err := c.ShouldBindJSON(&service); err == nil { + res := service.Update(c, CurrentUser(c)) + c.JSON(200, res) + } else { + c.JSON(200, ErrorResponse(err)) + } +} + // CreateWebDAVAccounts 创建WebDAV账户 func CreateWebDAVAccounts(c *gin.Context) { var service setting.WebDAVAccountCreateService diff --git a/routers/router.go b/routers/router.go index 53b1683..d28a93b 100644 --- a/routers/router.go +++ b/routers/router.go @@ -699,6 +699,8 @@ func InitMasterRouter() *gin.Engine { webdav.POST("accounts", controllers.CreateWebDAVAccounts) // 删除账号 webdav.DELETE("accounts/:id", controllers.DeleteWebDAVAccounts) + // 更新账号可读性 + webdav.PATCH("accounts", controllers.UpdateWebDAVAccountsReadonly) } } diff --git a/service/admin/file.go b/service/admin/file.go index 84d8129..a029989 100644 --- a/service/admin/file.go +++ b/service/admin/file.go @@ -19,8 +19,9 @@ type FileService struct { // FileBatchService 文件批量操作服务 type FileBatchService struct { - ID []uint `json:"id" binding:"min=1"` - Force bool `json:"force"` + ID []uint `json:"id" binding:"min=1"` + Force bool `json:"force"` + UnlinkOnly bool `json:"unlink"` } // ListFolderService 列目录结构 @@ -103,15 +104,22 @@ func (service *FileBatchService) Delete(c *gin.Context) serializer.Response { // 异步执行删除 go func(files map[uint][]model.File) { for uid, file := range files { + var ( + fs *filesystem.FileSystem + err error + ) user, err := model.GetUserByID(uid) if err != nil { - continue - } - - fs, err := filesystem.NewFileSystem(&user) - if err != nil { - fs.Recycle() - continue + fs, err = filesystem.NewAnonymousFileSystem() + if err != nil { + continue + } + } else { + fs, err = filesystem.NewFileSystem(&user) + if err != nil { + fs.Recycle() + continue + } } // 汇总文件ID @@ -121,7 +129,7 @@ func (service *FileBatchService) Delete(c *gin.Context) serializer.Response { } // 执行删除 - fs.Delete(context.Background(), []uint{}, ids, service.Force) + fs.Delete(context.Background(), []uint{}, ids, service.Force, service.UnlinkOnly) fs.Recycle() } }(userFile) diff --git a/service/admin/user.go b/service/admin/user.go index 9ade2ed..0b66deb 100644 --- a/service/admin/user.go +++ b/service/admin/user.go @@ -66,7 +66,7 @@ func (service *UserBatchService) Delete() serializer.Response { if err != nil { return serializer.Err(serializer.CodeInternalSetting, "User's root folder not exist", err) } - fs.Delete(context.Background(), []uint{root.ID}, []uint{}, false) + fs.Delete(context.Background(), []uint{root.ID}, []uint{}, false, false) // 删除相关任务 model.DB.Where("user_id = ?", uid).Delete(&model.Download{}) diff --git a/service/explorer/objects.go b/service/explorer/objects.go index 5099c7e..1c3c45a 100644 --- a/service/explorer/objects.go +++ b/service/explorer/objects.go @@ -41,9 +41,11 @@ type ItemService struct { // ItemIDService 处理多文件/目录相关服务,字段值为HashID,可通过Raw()方法获取原始ID type ItemIDService struct { - Items []string `json:"items"` - Dirs []string `json:"dirs"` - Source *ItemService + Items []string `json:"items"` + Dirs []string `json:"dirs"` + Source *ItemService + Force bool `json:"force"` + UnlinkOnly bool `json:"unlink"` } // ItemCompressService 文件压缩任务服务 @@ -272,9 +274,15 @@ func (service *ItemIDService) Delete(ctx context.Context, c *gin.Context) serial } defer fs.Recycle() + force, unlink := false, false + if fs.User.Group.OptionsSerialized.AdvanceDelete { + force = service.Force + unlink = service.UnlinkOnly + } + // 删除对象 items := service.Raw() - err = fs.Delete(ctx, items.Dirs, items.Items, false) + err = fs.Delete(ctx, items.Dirs, items.Items, force, unlink) if err != nil { return serializer.Err(serializer.CodeNotSet, err.Error(), err) } diff --git a/service/explorer/upload.go b/service/explorer/upload.go index b3501be..0cac8df 100644 --- a/service/explorer/upload.go +++ b/service/explorer/upload.go @@ -237,7 +237,7 @@ func (service *UploadSessionService) Delete(ctx context.Context, c *gin.Context) } // 删除文件 - if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false); err != nil { + if err := fs.Delete(ctx, []uint{}, []uint{file.ID}, false, false); err != nil { return serializer.Err(serializer.CodeInternalSetting, "Failed to delete upload session", err) } @@ -283,7 +283,7 @@ func DeleteAllUploadSession(ctx context.Context, c *gin.Context) serializer.Resp } // 删除文件 - if err := fs.Delete(ctx, []uint{}, fileIDs, false); err != nil { + if err := fs.Delete(ctx, []uint{}, fileIDs, false, false); err != nil { return serializer.Err(serializer.CodeInternalSetting, "Failed to cleanup upload session", err) } diff --git a/service/setting/webdav.go b/service/setting/webdav.go index f2d7e8f..4f252ad 100644 --- a/service/setting/webdav.go +++ b/service/setting/webdav.go @@ -22,6 +22,12 @@ type WebDAVAccountCreateService struct { Name string `json:"name" binding:"required,min=1,max=255"` } +// WebDAVAccountUpdateReadonlyService WebDAV 修改只读性服务 +type WebDAVAccountUpdateReadonlyService struct { + ID uint `json:"id" binding:"required,min=1"` + Readonly bool `json:"readonly"` +} + // WebDAVMountCreateService WebDAV 挂载创建服务 type WebDAVMountCreateService struct { Path string `json:"path" binding:"required,min=1,max=65535"` @@ -56,6 +62,14 @@ func (service *WebDAVAccountService) Delete(c *gin.Context, user *model.User) se return serializer.Response{} } +// Update 修改WebDAV账户的只读性 +func (service *WebDAVAccountUpdateReadonlyService) Update(c *gin.Context, user *model.User) serializer.Response { + model.UpdateWebDAVAccountReadonlyByID(service.ID, user.ID, service.Readonly) + return serializer.Response{Data: map[string]bool{ + "readonly": service.Readonly, + }} +} + // Accounts 列出WebDAV账号 func (service *WebDAVListService) Accounts(c *gin.Context, user *model.User) serializer.Response { accounts := model.ListWebDAVAccounts(user.ID)