working db scanner
This commit is contained in:
+2
-1
@@ -1,5 +1,6 @@
|
||||
*.env
|
||||
*.sh
|
||||
*.sqlite3
|
||||
.data
|
||||
zampler.files
|
||||
zampler.sqlite3
|
||||
internal/server/static
|
||||
|
||||
@@ -5,20 +5,29 @@ go 1.25.7
|
||||
require (
|
||||
code.aneur.in/go/record v0.0.0-20260704022026-db3f74ccdeb4
|
||||
code.aneur.in/go/rest v0.0.0-20260710144250-d3e2b415e4ac
|
||||
code.aneur.in/go/sqaffold v0.0.0-20260831231301-efa486fea8fa
|
||||
code.aneur.in/go/sqan v0.0.0-20260831231908-19f540e808b6
|
||||
code.aneur.in/go/sqimple v0.0.0-20260710160754-2a04163d6eff
|
||||
code.aneur.in/go/validate v0.0.0-20260705223845-8fab67ff77fe
|
||||
code.aneur.in/go/version v0.0.0-20260706210436-9f229dde97c9
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/spf13/cobra v1.10.2
|
||||
modernc.org/sqlite v1.57.0
|
||||
)
|
||||
|
||||
require (
|
||||
code.aneur.in/go/sqan v0.0.0-20260831212500-d13ee5cf13d1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/gobwas/glob v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-isatty v0.0.24 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
modernc.org/libc v1.74.4 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
|
||||
@@ -2,29 +2,49 @@ code.aneur.in/go/record v0.0.0-20260704022026-db3f74ccdeb4 h1:cE8tCjhHePkEMBEQbk
|
||||
code.aneur.in/go/record v0.0.0-20260704022026-db3f74ccdeb4/go.mod h1:nIqf1d1YWOPSVrbBFJY2ZBEMMFSfQCgXL7nFVXOzVqo=
|
||||
code.aneur.in/go/rest v0.0.0-20260710144250-d3e2b415e4ac h1:rmdYhq+CsQzPqK/tQ6G/DtWIS+IIME47Draei/mR9BM=
|
||||
code.aneur.in/go/rest v0.0.0-20260710144250-d3e2b415e4ac/go.mod h1:HtXodmPBkxQyYkXn4UHURJEs44a+Jm+4tUaKXj246uE=
|
||||
code.aneur.in/go/sqan v0.0.0-20260831212500-d13ee5cf13d1 h1:gLmIh9vibSVDqVeIjnukD3wTn2Nc65A3F+ciEnzcFzE=
|
||||
code.aneur.in/go/sqan v0.0.0-20260831212500-d13ee5cf13d1/go.mod h1:0o6TJqTFx7qPLHiUtUxeN2cLR/xylxtJZyghXamEfy4=
|
||||
code.aneur.in/go/sqaffold v0.0.0-20260831231301-efa486fea8fa h1:97WLloin1C/OOPQYyyALij+Prsa82gxw0kBvkDnEIb8=
|
||||
code.aneur.in/go/sqaffold v0.0.0-20260831231301-efa486fea8fa/go.mod h1:a3LqC1+uDSP2adLySuBEQmk/t2HJ+BNmIVMF+eSdC5s=
|
||||
code.aneur.in/go/sqan v0.0.0-20260831231908-19f540e808b6 h1:zuxg66WgJpxH0dHemFJasJ2i82jnSxL8c3FJdOwQInA=
|
||||
code.aneur.in/go/sqan v0.0.0-20260831231908-19f540e808b6/go.mod h1:0o6TJqTFx7qPLHiUtUxeN2cLR/xylxtJZyghXamEfy4=
|
||||
code.aneur.in/go/sqimple v0.0.0-20260710160754-2a04163d6eff h1:DAZPseBNreEYH4XQG087NfQ+PpHIlpYVThRXKmRkupc=
|
||||
code.aneur.in/go/sqimple v0.0.0-20260710160754-2a04163d6eff/go.mod h1:WT64312MyLtL4EiJWRyBFh+Tl//ls2+B+YsFV+BkPJ0=
|
||||
code.aneur.in/go/validate v0.0.0-20260705223845-8fab67ff77fe h1:jGSnKHtPM6mmcBjucsqduISqzhnPwZ7s1pTA6D7RWxg=
|
||||
code.aneur.in/go/validate v0.0.0-20260705223845-8fab67ff77fe/go.mod h1:xGuqmQj25KOUB+vD2L3Z+EDmdhZZB8ZRJK4Csj+LW6U=
|
||||
code.aneur.in/go/version v0.0.0-20260706210436-9f229dde97c9 h1:OquK0VYWHUts7u/23A6IiZ0PkznhIihj9U0DBSXfeM0=
|
||||
code.aneur.in/go/version v0.0.0-20260706210436-9f229dde97c9/go.mod h1:G26bUe3Jroua1UDRzJO12ssn6OPcTsizffKJ+5/FK7U=
|
||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
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/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/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
|
||||
github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3 h1:LMLX+LgTNWpfvCBdFebv6EsYotImrt/Ppc5cXIriCSo=
|
||||
github.com/google/pprof v0.0.0-20260802141513-ef3492d7dac3/go.mod h1:jl5iWTm0/hd5PjEYEOuwAJ57L/CibdZfrqZ5XA5GrCk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
|
||||
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
@@ -36,9 +56,42 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
|
||||
modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
|
||||
modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
|
||||
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.74.4 h1:fX1Omw4o2/1C2iRkkIsrQTasJQldLhRmuPreXLoWs9k=
|
||||
modernc.org/libc v1.74.4/go.mod h1:eeQAS9W3sZeKYMFubydxJpII9ybHWshk+7or7bLG9co=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.57.0 h1:qNQP6xnx5M0ISNtlnxoOX0+cD5bJ0/gr9aMmndFczzg=
|
||||
modernc.org/sqlite v1.57.0/go.mod h1:yCJ2cmAaIkHQ25oXWrF8H4O1lIfPYPR26yCEDj2P3pQ=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
+3
-3
@@ -3,13 +3,13 @@ package api
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.aneur.in/zampler/zampler/internal/samples"
|
||||
"code.aneur.in/zampler/zampler/internal/database"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type API struct {
|
||||
db *samples.DB
|
||||
db *database.DB
|
||||
|
||||
log zerolog.Logger
|
||||
}
|
||||
@@ -20,7 +20,7 @@ func (api *API) ConfigureRouter(r *mux.Router) {
|
||||
r.Path("/file/{id}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(api.GetFile)
|
||||
}
|
||||
|
||||
func NewAPI(db *samples.DB, log zerolog.Logger) *API {
|
||||
func NewAPI(db *database.DB, log zerolog.Logger) *API {
|
||||
api := &API{
|
||||
db: db,
|
||||
|
||||
|
||||
+43
-29
@@ -19,37 +19,33 @@ func (api *API) GetFile(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
|
||||
id := vars["id"]
|
||||
file, ok := api.db.Get(id)
|
||||
|
||||
if !ok {
|
||||
rest.ErrNotFound.WriteJSON(w)
|
||||
record, err := api.db.Select("files").Where("id = ?", id).QueryRecord()
|
||||
if err != nil {
|
||||
rest.WriteErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
rest.WriteResponseJSON(w, http.StatusOK, file.DTO())
|
||||
rest.WriteResponseJSON(w, http.StatusOK, record)
|
||||
}
|
||||
|
||||
func (api *API) GetFileByPath(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
|
||||
file, ok := api.db.GetByPath(fmt.Sprintf("/%s", vars["path"]))
|
||||
|
||||
if !ok {
|
||||
rest.ErrNotFound.WriteJSON(w)
|
||||
return
|
||||
record, err := api.db.Select("files").Where("rel_path = ?", fmt.Sprintf("/%s", vars["path"])).QueryRecord()
|
||||
if err != nil {
|
||||
rest.WriteErrorJSON(w, err)
|
||||
}
|
||||
|
||||
rest.WriteResponseJSON(w, http.StatusOK, file.DTO())
|
||||
rest.WriteResponseJSON(w, http.StatusOK, record)
|
||||
}
|
||||
|
||||
func (api *API) ListFiles(w http.ResponseWriter, req *http.Request) {
|
||||
uq := req.URL.Query()
|
||||
// q := api.db.Query()
|
||||
|
||||
// Read in params
|
||||
filename := ""
|
||||
path := ""
|
||||
asc := true
|
||||
direction := "asc"
|
||||
sort := "path"
|
||||
offset := 0
|
||||
limit := MaxLimit
|
||||
@@ -62,9 +58,9 @@ func (api *API) ListFiles(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
switch uq.Get("direction") {
|
||||
case "", "asc":
|
||||
asc = true
|
||||
direction = "asc"
|
||||
case "desc":
|
||||
asc = false
|
||||
direction = "desc"
|
||||
default:
|
||||
rest.ErrBadRequest.WithMessage("invalid sort direction").WriteJSON(w)
|
||||
return
|
||||
@@ -108,30 +104,48 @@ func (api *API) ListFiles(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
// Apply params
|
||||
q := api.db.Query()
|
||||
|
||||
if filename != "" {
|
||||
q.SearchFilename(filename)
|
||||
} else if path != "" {
|
||||
q.SearchPath(path)
|
||||
}
|
||||
|
||||
totalCount := q.Count()
|
||||
query := api.db.Select("files")
|
||||
|
||||
switch sort {
|
||||
case "filename":
|
||||
q.SortByFilename(asc)
|
||||
query.OrderBy("filename asc")
|
||||
case "path":
|
||||
q.SortByPath(asc)
|
||||
query.OrderBy("abs_path asc")
|
||||
}
|
||||
|
||||
q.Offset(offset).Limit(limit)
|
||||
query.Offset(offset).Limit(limit)
|
||||
|
||||
rows, err := query.Query()
|
||||
if err != nil {
|
||||
rest.WriteErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
totalCount := 0
|
||||
for rows.Next() {
|
||||
totalCount++
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
rest.WriteErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
if filename != "" {
|
||||
query.Where("filename like ?", filename)
|
||||
} else if path != "" {
|
||||
query.Where("abs_path like ?", path)
|
||||
}
|
||||
|
||||
records, err := query.QueryRecords()
|
||||
if err != nil {
|
||||
rest.WriteErrorJSON(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Output result
|
||||
output := dto.NewList(q.ResultDTOs()...).
|
||||
output := dto.NewList(records...).
|
||||
WithTotalCount(totalCount).
|
||||
WithParam("sort", sort).
|
||||
WithDirection(asc).
|
||||
WithDirection(direction).
|
||||
WithParam("offset", offset).
|
||||
WithParam("limit", limit).
|
||||
MaybeWithParam("filename", filename).
|
||||
|
||||
+28
-14
@@ -3,29 +3,33 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.aneur.in/go/sqan"
|
||||
"code.aneur.in/zampler/zampler/internal/api"
|
||||
"code.aneur.in/zampler/zampler/internal/db"
|
||||
"code.aneur.in/zampler/zampler/internal/samples"
|
||||
"code.aneur.in/zampler/zampler/internal/database"
|
||||
"code.aneur.in/zampler/zampler/internal/server"
|
||||
"code.aneur.in/zampler/zampler/internal/server/middlewares"
|
||||
"code.aneur.in/zampler/zampler/internal/types"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
type CLI struct {
|
||||
db *db.DB
|
||||
db *database.DB
|
||||
log zerolog.Logger
|
||||
}
|
||||
|
||||
func (cli *CLI) Cmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "zampler root-dir ...",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: cli.PreRunE,
|
||||
RunE: cli.RunE,
|
||||
}
|
||||
@@ -34,9 +38,10 @@ func (cli *CLI) Cmd() *cobra.Command {
|
||||
|
||||
f.StringP("address", "a", "", "HTTP bind address")
|
||||
f.IntP("port", "p", 7777, "HTTP listen port")
|
||||
|
||||
f.StringP("url", "u", "", "frontend URL")
|
||||
|
||||
f.StringP("sqlite", "s", "zampler.sqlite3", "path to SQLite database")
|
||||
|
||||
f.StringP("log-level", "l", "info", "logging level (trace, debug, info, warn, error)")
|
||||
|
||||
return cmd
|
||||
@@ -51,8 +56,6 @@ func (cli *CLI) PreRunE(cmd *cobra.Command, _ []string) error {
|
||||
|
||||
cli.log = cli.log.Level(level)
|
||||
|
||||
cli.db = samples.NewDB(cli.log)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -60,18 +63,22 @@ func (cli *CLI) RunE(cmd *cobra.Command, args []string) error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
dbFile, rootDir := args[0], args[1]
|
||||
host, _ := cmd.Flags().GetString("host")
|
||||
port, _ := cmd.Flags().GetInt("port")
|
||||
url, _ := cmd.Flags().GetString("url")
|
||||
|
||||
dbFile, _ := cmd.Flags().GetString("sqlite")
|
||||
|
||||
rootDir := args[0]
|
||||
|
||||
// Init database
|
||||
conn, err := sql.Open("sqlite", dbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cli.db = db.New(ctx, cli.log, conn)
|
||||
cli.db = database.New(ctx, cli.log, conn)
|
||||
if err := cli.db.Migrate(); err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -102,12 +109,19 @@ func (cli *CLI) RunE(cmd *cobra.Command, args []string) error {
|
||||
|
||||
// Initial scan of root directories
|
||||
go func() {
|
||||
files, err := cli.db.MultiScan(rootDirs...)
|
||||
if err != nil {
|
||||
errch <- err
|
||||
scanner := sqan.NewScanner(rootDir).WithExtensions("aac", "flac", "m4a", "mp3", "ogg", "wav")
|
||||
for simpleFile := range scanner.Scan() {
|
||||
file, err := types.NewFile(simpleFile)
|
||||
if err != nil {
|
||||
cli.log.Err(err).Send()
|
||||
continue
|
||||
}
|
||||
if err := cli.db.UpdateFile(file); err != nil {
|
||||
cli.log.Err(err).Send()
|
||||
continue
|
||||
}
|
||||
cli.log.Trace().Msgf("Scanned %s", file.RelativePath)
|
||||
}
|
||||
|
||||
cli.db.Import(files)
|
||||
}()
|
||||
|
||||
cli.log.Info().Int("port", port).Msg("Listening for HTTP connections")
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"code.aneur.in/go/record"
|
||||
"code.aneur.in/go/sqaffold"
|
||||
"code.aneur.in/go/sqimple"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
ctx context.Context
|
||||
log zerolog.Logger
|
||||
conn *sql.DB
|
||||
|
||||
scaffold *sqaffold.Scaffold
|
||||
}
|
||||
|
||||
func (db *DB) Delete(from string) *sqimple.DeleteStatement {
|
||||
return sqimple.Delete(from).ExecFunc(db.sqimpleExec)
|
||||
}
|
||||
|
||||
func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
|
||||
return db.scaffold.Exec(query, args...)
|
||||
}
|
||||
|
||||
func (db *DB) Insert(into string) *sqimple.InsertStatement {
|
||||
return sqimple.Insert(into).ExecFunc(db.sqimpleExec)
|
||||
}
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
return db.scaffold.Migrate()
|
||||
}
|
||||
|
||||
func (db *DB) Query(query string, args ...any) ([]record.Record, error) {
|
||||
return db.scaffold.Query(query, args...)
|
||||
}
|
||||
|
||||
func (db *DB) QueryOne(query string, args ...any) (record.Record, error) {
|
||||
return db.scaffold.QueryOne(query, args...)
|
||||
}
|
||||
|
||||
func (db *DB) Select(from string) *sqimple.SelectQuery {
|
||||
return sqimple.Select(from).QueryFunc(db.sqimpleQuery).QueryRowFunc(db.sqimpleQueryRow)
|
||||
}
|
||||
|
||||
func (db *DB) Update(table string) *sqimple.UpdateStatement {
|
||||
return sqimple.Update(table).ExecFunc(db.sqimpleExec)
|
||||
}
|
||||
|
||||
func (db *DB) sqimpleExec(query sqimple.Query) (sql.Result, error) {
|
||||
return db.Exec(query.String(), query.Args()...)
|
||||
}
|
||||
|
||||
func (db *DB) sqimpleQuery(query sqimple.Query) (*sql.Rows, error) {
|
||||
return db.conn.QueryContext(db.ctx, query.String(), query.Args()...)
|
||||
}
|
||||
|
||||
func (db *DB) sqimpleQueryRow(query sqimple.Query) *sql.Row {
|
||||
return db.conn.QueryRowContext(db.ctx, query.String(), query.Args()...)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, log zerolog.Logger, conn *sql.DB) *DB {
|
||||
db := &DB{
|
||||
ctx: ctx,
|
||||
log: log,
|
||||
conn: conn,
|
||||
}
|
||||
|
||||
db.scaffold = sqaffold.NewScaffold(db.ctx, conn).WithApp("zampler", db.migration())
|
||||
|
||||
return db
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"code.aneur.in/zampler/zampler/internal/types"
|
||||
)
|
||||
|
||||
func (db *DB) UpdateFile(file *types.File) error {
|
||||
_, err := db.Insert("files").
|
||||
Columns("id", "hash", "root_dir", "abs_path", "rel_dir", "rel_path", "filename", "ext", "size", "modified").
|
||||
Values(map[string]any{
|
||||
"id": file.ID,
|
||||
"hash": file.Hash,
|
||||
"root_dir": file.RootDir,
|
||||
"abs_path": file.AbsolutePath,
|
||||
"rel_dir": file.RelativeDir,
|
||||
"rel_path": file.RelativePath,
|
||||
"filename": file.Filename,
|
||||
"ext": file.Extension,
|
||||
"size": file.Size,
|
||||
"modified": file.Modified.UnixMilli(),
|
||||
}).
|
||||
Exec()
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
import "code.aneur.in/go/version"
|
||||
|
||||
func (db *DB) migration() version.Migration {
|
||||
return version.Migration{
|
||||
"0.1.0": version.Patch{
|
||||
Up: func() error {
|
||||
_, err := db.Exec(`
|
||||
create table files (
|
||||
id text not null,
|
||||
hash text,
|
||||
root_dir text not null,
|
||||
abs_path text not null,
|
||||
rel_dir text not null,
|
||||
rel_path text not null,
|
||||
filename text not null,
|
||||
ext text not null,
|
||||
size integer not null,
|
||||
modified integer not null,
|
||||
constraint samples__unq__id unique (id) on conflict replace
|
||||
)`)
|
||||
|
||||
return err
|
||||
},
|
||||
|
||||
Down: func() error {
|
||||
_, err := db.Exec("drop table files")
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package db
|
||||
|
||||
import "code.aneur.in/zampler/zampler/internal/types"
|
||||
|
||||
func (db *DB) GetConfig(name string) (*types.Config, error) {
|
||||
row, err := db.QueryOne("select name, value from config where name = ?", name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewConfig(row), nil
|
||||
}
|
||||
|
||||
func (db *DB) SetConfig(config *types.Config) error {
|
||||
_, err := db.Exec("insert into config (name, value) values (?, ?)", config.Name(), config.Value())
|
||||
return err
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"code.aneur.in/go/record"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
Ctx context.Context
|
||||
Log zerolog.Logger
|
||||
SQL *sql.DB
|
||||
}
|
||||
|
||||
func (db *DB) Exec(query string, args ...any) (sql.Result, error) {
|
||||
return db.SQL.ExecContext(db.Ctx, query, args...)
|
||||
}
|
||||
|
||||
func (db *DB) Query(query string, args ...any) ([]record.Record, error) {
|
||||
rows, err := db.SQL.QueryContext(db.Ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, Err(err)
|
||||
}
|
||||
|
||||
return ScanRecords(rows)
|
||||
}
|
||||
|
||||
func (db *DB) QueryOne(query string, args ...any) (record.Record, error) {
|
||||
rows, err := db.SQL.QueryContext(db.Ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, Err(err)
|
||||
}
|
||||
|
||||
return ScanOneRecord(rows)
|
||||
}
|
||||
|
||||
func New(ctx context.Context, log zerolog.Logger, sql *sql.DB) *DB {
|
||||
db := &DB{
|
||||
Ctx: ctx,
|
||||
Log: log,
|
||||
SQL: sql,
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package db
|
||||
|
||||
type Error struct {
|
||||
StatusCode int
|
||||
Text string
|
||||
|
||||
Previous error
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Text
|
||||
}
|
||||
|
||||
func (e *Error) Is(target error) bool {
|
||||
if other, ok := target.(*Error); ok {
|
||||
if other.StatusCode > 0 {
|
||||
if e.StatusCode != other.StatusCode {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if other.Text != "" {
|
||||
if e.Text != other.Text {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func Err(err error) *Error {
|
||||
return NewError(501, err.Error(), err)
|
||||
}
|
||||
|
||||
func NewError(statusCode int, text string, previous error) *Error {
|
||||
err := &Error{
|
||||
StatusCode: statusCode,
|
||||
Text: text,
|
||||
Previous: previous,
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"code.aneur.in/go/record"
|
||||
"code.aneur.in/go/version"
|
||||
"code.aneur.in/zampler/zampler/internal/types"
|
||||
)
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
currentVersion, err := db.GetVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
migration, err := db.Migration().Match(&version.Constraint{Gt: currentVersion})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versions, _ := migration.Versions()
|
||||
if len(versions) == 0 {
|
||||
db.Log.Info().Msg("No migrations needed")
|
||||
} else {
|
||||
db.Log.Info().Msg("Migrating")
|
||||
|
||||
var updateVersion *version.Version
|
||||
migrateErr := migration.Up(func(applied, next *version.Version) {
|
||||
updateVersion = applied
|
||||
|
||||
db.Log.Info().Msgf("Migrated to v%s", applied)
|
||||
})
|
||||
|
||||
if updateVersion != nil {
|
||||
config := types.NewConfig(record.Record{"name": "version", "value": updateVersion.Text})
|
||||
|
||||
if err := db.SetConfig(config); err != nil {
|
||||
db.Log.Err(err).Msg("Failed to save version")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if migrateErr != nil {
|
||||
db.Log.Err(migrateErr).Msg("Migration failed")
|
||||
|
||||
return migrateErr
|
||||
}
|
||||
|
||||
db.Log.Info().Msg("Migration complete")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *DB) RequiresMigration() (bool, error) {
|
||||
currentVersion, err := db.GetVersion()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
codeVersion := db.GetVersionInCode()
|
||||
|
||||
return currentVersion.Less(codeVersion), nil
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package db
|
||||
|
||||
import "code.aneur.in/go/version"
|
||||
|
||||
func (db *DB) Migration() *version.Migration {
|
||||
return &version.Migration{
|
||||
"0.1.0": version.Patch{
|
||||
Up: func() error {
|
||||
_, err := db.SQL.ExecContext(db.Ctx, `
|
||||
create table config (
|
||||
name text not null,
|
||||
value text,
|
||||
constraint config__unq__name unique (name) on conflict replace
|
||||
)`)
|
||||
|
||||
return err
|
||||
},
|
||||
|
||||
Down: func() error {
|
||||
_, err := db.SQL.ExecContext(db.Ctx, "drop table config")
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"code.aneur.in/go/record"
|
||||
)
|
||||
|
||||
func ScanOneRecord(rows *sql.Rows) (record.Record, error) {
|
||||
records, err := ScanRecords(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(records) == 0 {
|
||||
return nil, errors.New("no rows")
|
||||
}
|
||||
|
||||
return records[0], nil
|
||||
}
|
||||
|
||||
// ScanRecords transforms SQL row data into record.Record objects, making them easier to handle with type safety.
|
||||
//
|
||||
// https://go.dev/ref/spec#Types
|
||||
func ScanRecords(rows *sql.Rows) ([]record.Record, error) {
|
||||
columns, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := []record.Record{}
|
||||
|
||||
for rows.Next() {
|
||||
values := []any{}
|
||||
for _, column := range columns {
|
||||
switch column.ScanType().Name() {
|
||||
case "bool":
|
||||
var value bool
|
||||
values = append(values, &value)
|
||||
case "byte":
|
||||
var value byte
|
||||
values = append(values, &value)
|
||||
case "complex64":
|
||||
var value complex64
|
||||
values = append(values, &value)
|
||||
case "complex128":
|
||||
var value complex128
|
||||
values = append(values, &value)
|
||||
case "float32":
|
||||
var value float32
|
||||
values = append(values, &value)
|
||||
case "float64":
|
||||
var value float64
|
||||
values = append(values, &value)
|
||||
case "int":
|
||||
var value int
|
||||
values = append(values, &value)
|
||||
case "int8":
|
||||
var value int8
|
||||
values = append(values, &value)
|
||||
case "int16":
|
||||
var value int16
|
||||
values = append(values, &value)
|
||||
case "int32":
|
||||
var value int32
|
||||
values = append(values, &value)
|
||||
case "int64":
|
||||
var value int64
|
||||
values = append(values, &value)
|
||||
case "rune":
|
||||
var value rune
|
||||
values = append(values, &value)
|
||||
case "string":
|
||||
var value string
|
||||
values = append(values, &value)
|
||||
case "uint":
|
||||
var value uint
|
||||
values = append(values, &value)
|
||||
case "uint8":
|
||||
var value uint8
|
||||
values = append(values, &value)
|
||||
case "uint16":
|
||||
var value uint16
|
||||
values = append(values, &value)
|
||||
case "uint32":
|
||||
var value uint32
|
||||
values = append(values, &value)
|
||||
case "uint64":
|
||||
var value uint64
|
||||
values = append(values, &value)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown scan type %q", column.ScanType().Name())
|
||||
}
|
||||
}
|
||||
|
||||
err := rows.Scan(values...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
row := record.Record{}
|
||||
for i, column := range columns {
|
||||
switch column.ScanType().Name() {
|
||||
case "bool":
|
||||
value := values[i].(*bool)
|
||||
row[column.Name()] = *value
|
||||
case "byte":
|
||||
value := values[i].(*byte)
|
||||
row[column.Name()] = *value
|
||||
case "complex64":
|
||||
value := values[i].(*complex64)
|
||||
row[column.Name()] = *value
|
||||
case "complex128":
|
||||
value := values[i].(*complex128)
|
||||
row[column.Name()] = *value
|
||||
case "float32":
|
||||
value := values[i].(*float32)
|
||||
row[column.Name()] = *value
|
||||
case "float64":
|
||||
value := values[i].(*float64)
|
||||
row[column.Name()] = *value
|
||||
case "int":
|
||||
value := values[i].(*int)
|
||||
row[column.Name()] = *value
|
||||
case "int8":
|
||||
value := values[i].(*int8)
|
||||
row[column.Name()] = *value
|
||||
case "int16":
|
||||
value := values[i].(*int16)
|
||||
row[column.Name()] = *value
|
||||
case "int32":
|
||||
value := values[i].(*int32)
|
||||
row[column.Name()] = *value
|
||||
case "int64":
|
||||
value := values[i].(*int64)
|
||||
row[column.Name()] = *value
|
||||
case "rune":
|
||||
value := values[i].(*rune)
|
||||
row[column.Name()] = *value
|
||||
case "string":
|
||||
value := values[i].(*string)
|
||||
row[column.Name()] = *value
|
||||
case "uint":
|
||||
value := values[i].(*uint)
|
||||
row[column.Name()] = *value
|
||||
case "uint8":
|
||||
value := values[i].(*uint8)
|
||||
row[column.Name()] = *value
|
||||
case "uint16":
|
||||
value := values[i].(*uint16)
|
||||
row[column.Name()] = *value
|
||||
case "uint32":
|
||||
value := values[i].(*uint32)
|
||||
row[column.Name()] = *value
|
||||
case "uint64":
|
||||
value := values[i].(*uint64)
|
||||
row[column.Name()] = *value
|
||||
}
|
||||
}
|
||||
|
||||
data = append(data, row)
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package db
|
||||
|
||||
import "code.aneur.in/go/version"
|
||||
|
||||
func (db *DB) GetVersion() (*version.Version, error) {
|
||||
config, err := db.GetConfig("version")
|
||||
if err != nil {
|
||||
switch err.Error() {
|
||||
case "SQL logic error: no such table: config (1)":
|
||||
return version.Parse("0")
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
return version.Parse("0")
|
||||
}
|
||||
|
||||
return version.Parse(config.Value())
|
||||
}
|
||||
|
||||
func (db *DB) GetVersionInCode() *version.Version {
|
||||
migrations, err := db.Migration().Versions()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return migrations[len(migrations)-1]
|
||||
}
|
||||
@@ -31,11 +31,8 @@ func (l *List[T]) WithCount(name string, value int) *List[T] {
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *List[T]) WithDirection(asc bool) *List[T] {
|
||||
if asc {
|
||||
return l.WithParam("direction", "asc")
|
||||
}
|
||||
return l.WithParam("direction", "desc")
|
||||
func (l *List[T]) WithDirection(direction string) *List[T] {
|
||||
return l.WithParam("direction", direction)
|
||||
}
|
||||
|
||||
func (l *List[T]) WithParam(name string, value any) *List[T] {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package samples
|
||||
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
log zerolog.Logger
|
||||
|
||||
files Files
|
||||
|
||||
pathIndex map[string]string
|
||||
}
|
||||
|
||||
func (db *DB) Files() map[string]*File {
|
||||
return db.files
|
||||
}
|
||||
|
||||
func (db *DB) Get(id string) (*File, bool) {
|
||||
if db.files[id] != nil {
|
||||
return db.files[id], true
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (db *DB) GetByPath(path string) (*File, bool) {
|
||||
if db.pathIndex[path] != "" {
|
||||
id := db.pathIndex[path]
|
||||
return db.Get(id)
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (db *DB) Import(files Files) {
|
||||
maps.Copy(db.files, files)
|
||||
|
||||
pathIndex := map[string]string{}
|
||||
for id, file := range files {
|
||||
pathIndex[file.Path] = id
|
||||
}
|
||||
|
||||
maps.Copy(db.pathIndex, pathIndex)
|
||||
}
|
||||
|
||||
func (db *DB) Query() *Query {
|
||||
return NewQuery(db.files)
|
||||
}
|
||||
|
||||
func NewDB(log zerolog.Logger) *DB {
|
||||
db := &DB{
|
||||
log: log.With().Str("namespace", "samples.DB").Logger(),
|
||||
|
||||
files: Files{},
|
||||
|
||||
pathIndex: map[string]string{},
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
package samples
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"code.aneur.in/zampler/zampler/internal/dto"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
RootDir string
|
||||
Path string
|
||||
|
||||
lastHash string
|
||||
}
|
||||
|
||||
func (file *File) AbsPath() string {
|
||||
return path.Join(file.RootDir, file.Path)
|
||||
}
|
||||
|
||||
func (file *File) DTO() *dto.File {
|
||||
return &dto.File{
|
||||
ID: file.ID(),
|
||||
Path: file.Path,
|
||||
Size: file.Size(),
|
||||
Hash: file.Hash(),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) Extension() string {
|
||||
return parseExtension(f.Path)
|
||||
}
|
||||
|
||||
func (file *File) Filename() string {
|
||||
return path.Base(file.Path)
|
||||
}
|
||||
|
||||
func (file *File) Hash() string {
|
||||
if file.lastHash == "" {
|
||||
file.lastHash, _ = file.safeHash()
|
||||
}
|
||||
|
||||
return file.lastHash
|
||||
}
|
||||
|
||||
func (f *File) ID() string {
|
||||
b := []byte(f.AbsPath())
|
||||
|
||||
sum := sha256.Sum256(b)
|
||||
|
||||
return hex.EncodeToString(sum[0:])
|
||||
}
|
||||
|
||||
func (f *File) Info() (os.FileInfo, error) {
|
||||
return os.Stat(f.AbsPath())
|
||||
}
|
||||
|
||||
func (f *File) Size() int64 {
|
||||
if info, err := f.Info(); err == nil {
|
||||
return info.Size()
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (file *File) String() string {
|
||||
return file.AbsPath()
|
||||
}
|
||||
|
||||
func (file *File) safeHash() (string, error) {
|
||||
b, err := os.ReadFile(file.AbsPath())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := sha256.Sum256(b)
|
||||
hash := hex.EncodeToString(sum[0:])
|
||||
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
func NewFile(rootDir, path string) (*File, error) {
|
||||
file := &File{
|
||||
RootDir: rootDir,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
hash, err := file.safeHash()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
file.lastHash = hash
|
||||
}
|
||||
|
||||
return file, nil
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package samples
|
||||
|
||||
type Files map[string]*File
|
||||
|
||||
func (files Files) PathMap() map[string]string {
|
||||
out := map[string]string{}
|
||||
|
||||
for _, file := range files {
|
||||
out[file.Path] = file.ID()
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package samples
|
||||
|
||||
import "strings"
|
||||
|
||||
func parseExtension(path string) string {
|
||||
if i := strings.LastIndexByte(path, '.'); i > -1 {
|
||||
return strings.ToLower(path[i+1:])
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
package samples
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"code.aneur.in/zampler/zampler/internal/dto"
|
||||
)
|
||||
|
||||
type Query struct {
|
||||
files []*File
|
||||
|
||||
sortAsc bool
|
||||
sortMode string
|
||||
}
|
||||
|
||||
// Len implements [sort.Interface].
|
||||
func (q *Query) Len() int {
|
||||
return len(q.files)
|
||||
}
|
||||
|
||||
// Less implements [sort.Interface].
|
||||
func (q *Query) Less(i int, j int) bool {
|
||||
a := q.files[i]
|
||||
b := q.files[j]
|
||||
|
||||
switch q.sortMode {
|
||||
case "filename":
|
||||
if q.sortAsc {
|
||||
return a.Filename() < b.Filename()
|
||||
} else {
|
||||
return b.Filename() < a.Filename()
|
||||
}
|
||||
case "path":
|
||||
if q.sortAsc {
|
||||
return a.Path < b.Path
|
||||
} else {
|
||||
return b.Path < a.Path
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Swap implements [sort.Interface].
|
||||
func (q *Query) Swap(i int, j int) {
|
||||
tmp := q.files[i]
|
||||
q.files[i] = q.files[j]
|
||||
q.files[j] = tmp
|
||||
}
|
||||
|
||||
func (q *Query) Count() int {
|
||||
return len(q.files)
|
||||
}
|
||||
|
||||
func (q *Query) Limit(num int) *Query {
|
||||
if num > len(q.files) {
|
||||
num = len(q.files)
|
||||
}
|
||||
|
||||
q.files = q.files[0:num]
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) Offset(num int) *Query {
|
||||
if num > len(q.files) {
|
||||
num = len(q.files)
|
||||
}
|
||||
|
||||
q.files = q.files[num:]
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) Result() []*File {
|
||||
return q.files
|
||||
}
|
||||
|
||||
func (q *Query) ResultDTOs() []*dto.File {
|
||||
dtos := []*dto.File{}
|
||||
|
||||
for _, file := range q.Result() {
|
||||
dtos = append(dtos, file.DTO())
|
||||
}
|
||||
|
||||
return dtos
|
||||
}
|
||||
|
||||
func (q *Query) SearchFilename(text string) *Query {
|
||||
text = strings.ToLower(text)
|
||||
|
||||
filtered := []*File{}
|
||||
|
||||
for _, file := range q.files {
|
||||
filename := strings.ToLower(file.Filename())
|
||||
if strings.Contains(filename, text) {
|
||||
filtered = append(filtered, file)
|
||||
}
|
||||
}
|
||||
|
||||
q.files = filtered
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) SearchPath(text string) *Query {
|
||||
text = strings.ToLower(text)
|
||||
|
||||
filtered := []*File{}
|
||||
|
||||
for _, file := range q.files {
|
||||
path := strings.ToLower(file.Path)
|
||||
if strings.Contains(path, text) {
|
||||
filtered = append(filtered, file)
|
||||
}
|
||||
}
|
||||
|
||||
q.files = filtered
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) SortByFilename(asc bool) *Query {
|
||||
q.sortAsc = asc
|
||||
q.sortMode = "filename"
|
||||
|
||||
sort.Stable(q)
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Query) SortByPath(asc bool) *Query {
|
||||
q.sortAsc = asc
|
||||
q.sortMode = "path"
|
||||
|
||||
sort.Stable(q)
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
func NewQuery(files Files) *Query {
|
||||
data := []*File{}
|
||||
for _, file := range files {
|
||||
data = append(data, file)
|
||||
}
|
||||
|
||||
q := &Query{
|
||||
files: data,
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package samples
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"maps"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
)
|
||||
|
||||
var ScanExtensions = []string{"aac", "flac", "m4a", "mp3", "ogg", "wav"}
|
||||
|
||||
func (db *DB) MultiScan(rootDirs ...string) (Files, error) {
|
||||
files := Files{}
|
||||
|
||||
for _, rootDir := range rootDirs {
|
||||
moreFiles, err := db.Scan(rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
maps.Copy(files, moreFiles)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (db *DB) Scan(rootDir string) (Files, error) {
|
||||
if !path.IsAbs(rootDir) {
|
||||
return nil, errors.New("rootDir must be absolute path")
|
||||
}
|
||||
|
||||
log := db.log.With().Str("rootDir", rootDir).Logger()
|
||||
|
||||
log.Debug().Msg("Scanning for files")
|
||||
paths, err := scanRecursive(rootDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := len(rootDir)
|
||||
files := Files{}
|
||||
for _, path := range paths {
|
||||
file, err := NewFile(rootDir, path[l:])
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("path", file.Path).Msg("Failed to read file")
|
||||
}
|
||||
|
||||
id := file.ID()
|
||||
files[id] = file
|
||||
|
||||
log.Trace().Str("id", id).Str("path", file.Path).Str("hash", file.Hash()).Msg("Found")
|
||||
}
|
||||
|
||||
log.Debug().Int("files", len(files)).Msg("Completed scan")
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func scanRecursive(dir string, exts ...string) ([]string, error) {
|
||||
paths := []string{}
|
||||
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
path := path.Join(dir, entry.Name())
|
||||
|
||||
if entry.IsDir() {
|
||||
subPaths, err := scanRecursive(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths = append(paths, subPaths...)
|
||||
} else {
|
||||
ext := parseExtension(path)
|
||||
if slices.Contains(ScanExtensions, ext) {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paths, nil
|
||||
}
|
||||
+41
-41
@@ -1,56 +1,56 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
// import (
|
||||
// "fmt"
|
||||
// "net/http"
|
||||
// "os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
// "github.com/gorilla/mux"
|
||||
// )
|
||||
|
||||
func (srv *Server) ReadFile(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
// func (srv *Server) ReadFile(w http.ResponseWriter, req *http.Request) {
|
||||
// vars := mux.Vars(req)
|
||||
|
||||
id := vars["id"]
|
||||
file, ok := srv.db.Get(id)
|
||||
// id := vars["id"]
|
||||
// file, ok := srv.db.Get(id)
|
||||
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte{})
|
||||
return
|
||||
}
|
||||
// if !ok {
|
||||
// w.WriteHeader(http.StatusNotFound)
|
||||
// w.Write([]byte{})
|
||||
// return
|
||||
// }
|
||||
|
||||
data, err := os.ReadFile(file.AbsPath())
|
||||
if err != nil {
|
||||
srv.log.Err(err).Msg("Failed to read file")
|
||||
// data, err := os.ReadFile(file.AbsPath())
|
||||
// if err != nil {
|
||||
// srv.log.Err(err).Msg("Failed to read file")
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte{})
|
||||
}
|
||||
// w.WriteHeader(http.StatusInternalServerError)
|
||||
// w.Write([]byte{})
|
||||
// }
|
||||
|
||||
w.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Filename()))
|
||||
w.Write(data)
|
||||
}
|
||||
// w.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Filename()))
|
||||
// w.Write(data)
|
||||
// }
|
||||
|
||||
func (srv *Server) ReadFileByPath(w http.ResponseWriter, req *http.Request) {
|
||||
vars := mux.Vars(req)
|
||||
// func (srv *Server) ReadFileByPath(w http.ResponseWriter, req *http.Request) {
|
||||
// vars := mux.Vars(req)
|
||||
|
||||
file, ok := srv.db.GetByPath(fmt.Sprintf("/%s", vars["path"]))
|
||||
// file, ok := srv.db.GetByPath(fmt.Sprintf("/%s", vars["path"]))
|
||||
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
w.Write([]byte{})
|
||||
return
|
||||
}
|
||||
// if !ok {
|
||||
// w.WriteHeader(http.StatusNotFound)
|
||||
// w.Write([]byte{})
|
||||
// return
|
||||
// }
|
||||
|
||||
data, err := os.ReadFile(file.AbsPath())
|
||||
if err != nil {
|
||||
srv.log.Err(err).Msg("Failed to read file")
|
||||
// data, err := os.ReadFile(file.AbsPath())
|
||||
// if err != nil {
|
||||
// srv.log.Err(err).Msg("Failed to read file")
|
||||
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte{})
|
||||
}
|
||||
// w.WriteHeader(http.StatusInternalServerError)
|
||||
// w.Write([]byte{})
|
||||
// }
|
||||
|
||||
w.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Filename()))
|
||||
w.Write(data)
|
||||
}
|
||||
// w.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", file.Filename()))
|
||||
// w.Write(data)
|
||||
// }
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.aneur.in/go/rest"
|
||||
"code.aneur.in/zampler/zampler/internal/database"
|
||||
"code.aneur.in/zampler/zampler/internal/dto"
|
||||
"code.aneur.in/zampler/zampler/internal/samples"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
@@ -19,7 +19,7 @@ var staticFiles embed.FS
|
||||
type Server struct {
|
||||
url string
|
||||
|
||||
db *samples.DB
|
||||
db *database.DB
|
||||
log zerolog.Logger
|
||||
|
||||
static http.Handler
|
||||
@@ -28,8 +28,8 @@ type Server struct {
|
||||
|
||||
func (srv *Server) ConfigureRouter(r *mux.Router) {
|
||||
r.Path("/config.json").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.GetConfig)
|
||||
r.Path("/files/{path:.+}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.ReadFileByPath)
|
||||
r.Path("/file/{id}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.ReadFile)
|
||||
// r.Path("/files/{path:.+}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.ReadFileByPath)
|
||||
// r.Path("/file/{id}").Methods(http.MethodGet, http.MethodOptions).HandlerFunc(srv.ReadFile)
|
||||
}
|
||||
|
||||
func (srv *Server) ConfigureStaticAssets(r *mux.Router) {
|
||||
@@ -46,7 +46,7 @@ func (srv *Server) GetConfig(w http.ResponseWriter, req *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
func NewServer(db *samples.DB, log zerolog.Logger, url string) *Server {
|
||||
func NewServer(db *database.DB, log zerolog.Logger, url string) *Server {
|
||||
srv := &Server{
|
||||
url: url,
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
package types
|
||||
|
||||
import "code.aneur.in/go/record"
|
||||
|
||||
type Config struct {
|
||||
Data record.Record
|
||||
}
|
||||
|
||||
func (config *Config) Name() string {
|
||||
return config.Data.String("name")
|
||||
}
|
||||
|
||||
func (config *Config) Value() string {
|
||||
return config.Data.String("value")
|
||||
}
|
||||
|
||||
func NewConfig(row record.Record) *Config {
|
||||
return &Config{Data: row}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.aneur.in/go/sqan"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
|
||||
RootDir string `json:"rootDir,omitempty"`
|
||||
AbsolutePath string `json:"absolutePath,omitempty"`
|
||||
RelativeDir string `json:"relativeDir,omitempty"`
|
||||
RelativePath string `json:"relativePath,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Extension string `json:"extension,omitempty"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
Modified time.Time `json:"modified"`
|
||||
}
|
||||
|
||||
func NewFile(other *sqan.File) (*File, error) {
|
||||
f := &File{
|
||||
RootDir: other.RootDir,
|
||||
AbsolutePath: other.AbsolutePath,
|
||||
RelativeDir: other.RelativeDir,
|
||||
RelativePath: other.RelativePath,
|
||||
Filename: other.Filename,
|
||||
Extension: other.Extension,
|
||||
Size: other.Size,
|
||||
Modified: other.Modified,
|
||||
}
|
||||
|
||||
b := []byte(f.AbsolutePath)
|
||||
sum := sha256.Sum256(b)
|
||||
f.ID = hex.EncodeToString(sum[0:])
|
||||
|
||||
b, err := os.ReadFile(f.AbsolutePath)
|
||||
if err == nil {
|
||||
sum = sha256.Sum256(b)
|
||||
f.Hash = hex.EncodeToString(sum[0:])
|
||||
}
|
||||
|
||||
return f, err
|
||||
}
|
||||
Reference in New Issue
Block a user