From 6eadc6503234189d639208c2e28c285f489a61ee Mon Sep 17 00:00:00 2001 From: Abhishek Shroff Date: Sun, 3 Mar 2024 15:30:32 +0530 Subject: [PATCH] Initial Commit --- .gitignore | 1 + cmd/phylum.go | 9 + go.mod | 61 ++++++ go.sum | 166 +++++++++++++++ internal/cmds/admin.go | 42 ++++ internal/cmds/root.go | 84 ++++++++ internal/cmds/serve.go | 98 +++++++++ internal/pgfs/contentstore.go | 14 ++ internal/pgfs/localfscontentstore.go | 59 ++++++ internal/pgfs/pgfs.go | 176 +++++++++++++++ internal/phylumsql/db.go | 32 +++ internal/phylumsql/models.go | 22 ++ internal/phylumsql/resources.sql.go | 294 ++++++++++++++++++++++++++ sql/migrations/001_resources.down.sql | 1 + sql/migrations/001_resources.up.sql | 14 ++ sql/queries/resources.sql | 69 ++++++ sqlc.yml | 21 ++ 17 files changed, 1163 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/phylum.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/cmds/admin.go create mode 100644 internal/cmds/root.go create mode 100644 internal/cmds/serve.go create mode 100644 internal/pgfs/contentstore.go create mode 100644 internal/pgfs/localfscontentstore.go create mode 100644 internal/pgfs/pgfs.go create mode 100644 internal/phylumsql/db.go create mode 100644 internal/phylumsql/models.go create mode 100644 internal/phylumsql/resources.sql.go create mode 100644 sql/migrations/001_resources.down.sql create mode 100644 sql/migrations/001_resources.up.sql create mode 100644 sql/queries/resources.sql create mode 100644 sqlc.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..567609b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/cmd/phylum.go b/cmd/phylum.go new file mode 100644 index 00000000..e037046b --- /dev/null +++ b/cmd/phylum.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/shroff/phylum/server/internal/cmds" +) + +func main() { + cmds.Setup() +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..2dfe4a75 --- /dev/null +++ b/go.mod @@ -0,0 +1,61 @@ +module github.com/shroff/phylum/server + +go 1.21.6 + +require ( + github.com/emersion/go-webdav v0.5.0 + github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-contrib/cors v1.5.0 + github.com/gin-gonic/gin v1.9.1 + github.com/google/uuid v1.4.0 + github.com/jackc/pgx/v5 v5.5.3 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.8.0 + github.com/spf13/viper v1.18.2 +) + +require ( + github.com/bytedance/sonic v1.10.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect + github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.15.5 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/arch v0.5.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..d3f6161a --- /dev/null +++ b/go.sum @@ -0,0 +1,166 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= +github.com/bytedance/sonic v1.10.1 h1:7a1wuFXL1cMy7a3f7/VFcEtriuXQnUBhtoVfOZiaysc= +github.com/bytedance/sonic v1.10.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emersion/go-ical v0.0.0-20220601085725-0864dccc089f/go.mod h1:2MKFUgfNMULRxqZkadG1Vh44we3y5gJAtTBlVsx1BKQ= +github.com/emersion/go-vcard v0.0.0-20230815062825-8fda7d206ec9/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM= +github.com/emersion/go-webdav v0.5.0 h1:Ak/BQLgAihJt/UxJbCsEXDPxS5Uw4nZzgIMOq3rkKjc= +github.com/emersion/go-webdav v0.5.0/go.mod h1:ycyIzTelG5pHln4t+Y32/zBvmrM7+mV7x+V+Gx4ZQno= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= +github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/cors v1.5.0 h1:DgGKV7DDoOn36DFkNtbHrjoRiT5ExCe+PC9/xp7aKvk= +github.com/gin-contrib/cors v1.5.0/go.mod h1:TvU7MAZ3EwrPLI2ztzTt3tqgvBCq+wn8WpZmfADjupI= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.15.5 h1:LEBecTWb/1j5TNY1YYG2RcOUN3R7NLylN+x8TTueE24= +github.com/go-playground/validator/v10 v10.15.5/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= +github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.3 h1:Ces6/M3wbDXYpM8JyyPD57ivTtJACFZJd885pdIaV2s= +github.com/jackc/pgx/v5 v5.5.3/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU= +github.com/teambition/rrule-go v1.8.2/go.mod h1:Ieq5AbrKGciP1V//Wq8ktsTXwSwJHDD5mD/wLBGl3p4= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.5.0 h1:jpGode6huXQxcskEIpOCvrU+tzo81b6+oFLUYXWtH/Y= +golang.org/x/arch v0.5.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/cmds/admin.go b/internal/cmds/admin.go new file mode 100644 index 00000000..0b896682 --- /dev/null +++ b/internal/cmds/admin.go @@ -0,0 +1,42 @@ +package cmds + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/shroff/phylum/server/internal/phylumsql" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func setupAdminCommand() *cobra.Command { + var cmdServe = &cobra.Command{ + Use: "admin", + Short: "Server Administration", + } + cmdServe.AddCommand([]*cobra.Command{setupAdminMkrootCommand()}...) + + return cmdServe +} + +func setupAdminMkrootCommand() *cobra.Command { + return &cobra.Command{ + Use: "mkroot name", + Short: "Create Root Folder", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + name := args[0] + if _, err := queries.FindRoot(context.Background(), name); err == nil { + log.Fatal(fmt.Sprintf("Root directory already exists: %s", name)) + } else { + _, err = queries.CreateDirectory(context.Background(), phylumsql.CreateDirectoryParams{ID: uuid.New(), Parent: nil, Name: name}) + if err != nil { + log.Fatal(err) + } else { + log.Info(fmt.Sprintf("Root directory created: %s", name)) + } + } + }, + } +} diff --git a/internal/cmds/root.go b/internal/cmds/root.go new file mode 100644 index 00000000..2c353ba5 --- /dev/null +++ b/internal/cmds/root.go @@ -0,0 +1,84 @@ +package cmds + +import ( + "context" + "fmt" + "os" + "path" + + "github.com/jackc/pgx/v5" + "github.com/shroff/phylum/server/internal/phylumsql" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var debugMode bool = false +var queries phylumsql.Queries + +func Setup() { + viper.SetEnvPrefix("phylum") + + var rootCmd = &cobra.Command{Use: path.Base(os.Args[0])} + flags := rootCmd.PersistentFlags() + + flags.BoolP("debug", "d", false, "Debug mode") + viper.BindPFlag("debug", flags.Lookup("debug")) + + flags.StringP("working-dir", "W", ".", "Working Directory") + viper.BindPFlag("working_dir", flags.Lookup("working-dir")) + + flags.String("database-url", "postgres://phylum:phylum@localhost:5432/phylum", "Database URL or DSN") + viper.BindPFlag("database_url", flags.Lookup("database-url")) + + var conn *pgx.Conn + + rootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { + workDir := viper.GetString("working_dir") + if workDir != "." { + log.Info(fmt.Sprintf("Setting working directory to %s", workDir)) + os.Mkdir(workDir, 0750) + os.Chdir(workDir) + } + debugMode = viper.GetBool("debug") + if debugMode { + log.Info("Running in debug mode") + log.SetLevel(log.TraceLevel) + } + + dsn := viper.GetString("database_url") + config, err := pgx.ParseConfig(dsn) + if err != nil { + log.Fatal(fmt.Sprintf("Unable to parse db connection String: %v\n", err)) + } + if debugMode { + config.Tracer = phylumTracer{} + } + conn, err = pgx.ConnectConfig(context.Background(), config) + if err != nil { + log.Fatal(fmt.Sprintf("Unable to connect to database: %v\n", err)) + } + queries = *phylumsql.New(conn) + } + + defer func() { + if conn != nil { + log.Info("Closing datbase connection") + conn.Close(context.Background()) + } + }() + + rootCmd.AddCommand([]*cobra.Command{setupServeCommand(), setupAdminCommand()}...) + rootCmd.Execute() +} + +type phylumTracer struct { +} + +func (p phylumTracer) TraceQueryStart(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryStartData) context.Context { + log.Trace(fmt.Sprintf("%+v\n", data)) + return ctx +} + +func (p phylumTracer) TraceQueryEnd(ctx context.Context, conn *pgx.Conn, data pgx.TraceQueryEndData) { +} diff --git a/internal/cmds/serve.go b/internal/cmds/serve.go new file mode 100644 index 00000000..ae73153a --- /dev/null +++ b/internal/cmds/serve.go @@ -0,0 +1,98 @@ +package cmds + +import ( + "fmt" + "time" + + webdav "github.com/emersion/go-webdav" + "github.com/fvbock/endless" + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" + "github.com/shroff/phylum/server/internal/pgfs" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +func setupServeCommand() *cobra.Command { + var cmdServe = &cobra.Command{ + Use: "serve", + Short: "Run the server", + Run: func(cmd *cobra.Command, args []string) { + config := viper.GetViper() + engine := createEngine(config.GetBool("cors_enabled"), config.GetStringSlice("cors_origins")) + + setupWebdav(engine.Group(config.GetString("webdav_prefix"))) + + server := endless.NewServer(config.GetString("listen"), engine) + server.BeforeBegin = func(addr string) { + log.Info(fmt.Sprintf("Listening on %s\n", addr)) + } + + if err := server.ListenAndServe(); err != nil { + log.Fatal(err.Error()) + } + }, + } + flags := cmdServe.Flags() + + flags.StringP("listen", "l", ":1234", "Listen Addres") + viper.BindPFlag("listen", flags.Lookup("listen")) + + flags.Bool("cors-enabled", false, "CORS enabled") + viper.BindPFlag("cors_enabled", flags.Lookup("cors-enabled")) + + flags.StringSlice("cors-origins", []string{"*"}, "CORS origins") + viper.BindPFlag("cors_origins", flags.Lookup("cors-origins")) + + flags.String("webdav-prefix", "/webdav", "Listen Addres") + viper.BindPFlag("webdav_prefix", flags.Lookup("webdav-prefix")) + + return cmdServe +} + +func setupWebdav(r *gin.RouterGroup) { + log.Info(fmt.Sprintf("Setting up WebDAV access at %s", r.BasePath())) + + cs, err := pgfs.NewLocalFsContentStore("srv") + if err != nil { + panic(err) + } + webdavHandler := webdav.Handler{ + FileSystem: pgfs.New(queries, cs, r.BasePath()), + } + handler := func(c *gin.Context) { + webdavHandler.ServeHTTP(c.Writer, c.Request) + } + r.Handle("OPTIONS", "/*path", handler) + r.Handle("GET", "/*path", handler) + r.Handle("PUT", "/*path", handler) + r.Handle("HEAD", "/*path", handler) + r.Handle("POST", "/*path", handler) + r.Handle("DELETE", "/*path", handler) + r.Handle("MOVE", "/*path", handler) + r.Handle("COPY", "/*path", handler) + r.Handle("PROPFIND", "/*path", handler) + r.Handle("PROPPATCH", "/*path", handler) +} + +func createEngine(corsEnabled bool, corsOrigins []string) *gin.Engine { + if !debugMode { + gin.SetMode(gin.ReleaseMode) + } + engine := gin.Default() + + if corsEnabled { + engine.Use(cors.New(cors.Config{ + AllowOrigins: corsOrigins, + AllowHeaders: []string{"Origin", "Authorization", "Accept", "Accept-Language", "Content-Type"}, + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "PROPFIND", "PROPPATCH", "COPY", "MOVE"}, + ExposeHeaders: []string{"Content-Length"}, + AllowWebSockets: true, + AllowCredentials: true, + MaxAge: 24 * time.Hour, + })) + } + + return engine +} diff --git a/internal/pgfs/contentstore.go b/internal/pgfs/contentstore.go new file mode 100644 index 00000000..fc5b1c35 --- /dev/null +++ b/internal/pgfs/contentstore.go @@ -0,0 +1,14 @@ +package pgfs + +import ( + "hash" + "io" + + "github.com/google/uuid" +) + +type ContentStore interface { + Open(id uuid.UUID) (io.ReadCloser, error) + Create(id uuid.UUID, callback func(hash.Hash, error)) (io.WriteCloser, error) + Delete(id uuid.UUID) error +} diff --git a/internal/pgfs/localfscontentstore.go b/internal/pgfs/localfscontentstore.go new file mode 100644 index 00000000..0ccc868a --- /dev/null +++ b/internal/pgfs/localfscontentstore.go @@ -0,0 +1,59 @@ +package pgfs + +import ( + "crypto/md5" + "hash" + "io" + "os" + "path/filepath" + + "github.com/google/uuid" +) + +type LocalFsContentStore string + +type contentWriter struct { + file *os.File + hash hash.Hash + callback func(hash.Hash, error) +} + +func (c contentWriter) Write(p []byte) (n int, err error) { + n, err = c.file.Write(p) + c.hash.Write(p) + return +} + +func (c contentWriter) Close() error { + err := c.file.Close() + c.callback(c.hash, err) + return err +} + +func NewLocalFsContentStore(root string) (ContentStore, error) { + err := os.MkdirAll(root, 0750) + if err != nil { + return nil, err + } + return LocalFsContentStore(root), nil +} + +func (l LocalFsContentStore) Open(id uuid.UUID) (io.ReadCloser, error) { + return os.Open(l.path(id)) +} + +func (l LocalFsContentStore) Create(id uuid.UUID, callback func(hash.Hash, error)) (io.WriteCloser, error) { + file, err := os.Create(l.path(id)) + if err != nil { + return nil, err + } + return contentWriter{file, md5.New(), callback}, nil +} + +func (l LocalFsContentStore) Delete(id uuid.UUID) error { + return os.Remove(filepath.Join(string(l), id.String())) +} + +func (l LocalFsContentStore) path(id uuid.UUID) string { + return filepath.Join(string(l), id.String()) +} diff --git a/internal/pgfs/pgfs.go b/internal/pgfs/pgfs.go new file mode 100644 index 00000000..31006d6a --- /dev/null +++ b/internal/pgfs/pgfs.go @@ -0,0 +1,176 @@ +package pgfs + +import ( + "context" + "encoding/base64" + "hash" + "io" + "io/fs" + "os" + "strings" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" + "github.com/shroff/phylum/server/internal/phylumsql" + + webdav "github.com/emersion/go-webdav" +) + +type Pgfs struct { + q phylumsql.Queries + cs ContentStore + prefix string +} + +func New(q phylumsql.Queries, cs ContentStore, prefix string) Pgfs { + return Pgfs{q: q, cs: cs, prefix: prefix} +} + +func (p Pgfs) Open(ctx context.Context, name string) (io.ReadCloser, error) { + resource, err := p.getResource(ctx, name) + if err != nil { + return nil, err + } + return p.cs.Open(resource.ID) +} +func (p Pgfs) Stat(ctx context.Context, name string) (*webdav.FileInfo, error) { + resource, err := p.getResource(ctx, name) + if err != nil { + return nil, err + } + val := &webdav.FileInfo{ + Path: string(resource.Path), + Size: int64(resource.Size.Int32), + ModTime: resource.Modified.Time, + IsDir: resource.Dir, + MIMEType: resource.Name, + ETag: resource.Etag.String, + } + return val, nil +} +func (p Pgfs) ReadDir(ctx context.Context, name string, recursive bool) ([]webdav.FileInfo, error) { + dir, err := p.getResource(ctx, name) + if err != nil { + return nil, err + } + if !dir.Dir { + return nil, fs.ErrInvalid + } + maxDepth := 1 + if recursive { + maxDepth = 1000 + } + children, err := p.q.ReadDir(ctx, phylumsql.ReadDirParams{PathPrefix: dir.Path + "/", ID: dir.ID, MaxDepth: int32(maxDepth)}) + if err != nil { + return nil, err + } + + result := make([]webdav.FileInfo, len(children)) + for i, c := range children { + result[i] = webdav.FileInfo{ + Path: string(c.Path), + Size: int64(c.Size.Int32), + ModTime: c.Modified.Time, + IsDir: c.Dir, + MIMEType: c.Name, + ETag: c.Etag.String, + } + } + return result, nil +} +func (p Pgfs) Create(ctx context.Context, name string) (io.WriteCloser, error) { + resource, _ := p.getResource(ctx, name) + if resource != nil { + return p.cs.Create(resource.ID, func(h hash.Hash, err error) { + if err != nil { + etag := base64.StdEncoding.EncodeToString(h.Sum(nil)) + _, err = p.q.UpdateResourceContents(ctx, phylumsql.UpdateResourceContentsParams{ + Size: pgtype.Int4{Int32: int32(h.Size()), Valid: true}, + Etag: pgtype.Text{String: string(etag), Valid: true}, + }) + } + if err != nil { + p.cs.Delete(resource.ID) + } + }) + } + index := strings.LastIndex(name, "/") + parentPath := name[0:index] + parent, err := p.getResource(ctx, parentPath) + if err != nil { + return nil, fs.ErrNotExist + } + fileName := name[index+1:] + id := uuid.New() + + return p.cs.Create(id, func(h hash.Hash, err error) { + etag := base64.StdEncoding.EncodeToString(h.Sum(nil)) + if err != nil { + _, err = p.q.CreateFile( + ctx, + phylumsql.CreateFileParams{ + ID: id, + Parent: &parent.ID, + Name: fileName, + Size: pgtype.Int4{Int32: int32(h.Size()), Valid: true}, + Etag: pgtype.Text{String: string(etag), Valid: true}, + }) + } + if err != nil { + p.cs.Delete(id) + } + }) +} + +func (p Pgfs) RemoveAll(ctx context.Context, name string) error { + resource, _ := p.getResource(ctx, name) + if resource == nil { + return fs.ErrNotExist + } + return p.q.DeleteRecursive(ctx, resource.ID) +} + +func (p Pgfs) Mkdir(ctx context.Context, name string) error { + resource, _ := p.getResource(ctx, name) + if resource != nil { + return fs.ErrExist + } + index := strings.LastIndex(name, "/") + parentPath := name[0:index] + parent, err := p.getResource(ctx, parentPath) + if err != nil { + return fs.ErrNotExist + } + dirName := name[index+1:] + _, err = p.q.CreateDirectory(ctx, phylumsql.CreateDirectoryParams{ID: uuid.New(), Parent: &parent.ID, Name: dirName}) + return err +} +func (p Pgfs) Copy(ctx context.Context, name, dest string, options *webdav.CopyOptions) (created bool, err error) { + // TODO: Implement + return false, nil +} +func (p Pgfs) Move(ctx context.Context, name, dest string, options *webdav.MoveOptions) (created bool, err error) { + // TODO: Implement + return false, nil +} + +func (p Pgfs) getResource(ctx context.Context, name string) (*phylumsql.ResourceByPathRow, error) { + path := strings.TrimPrefix(name, p.prefix+"/") + segments := strings.Split(strings.TrimRight(path, "/"), "/") + if len(segments) == 0 { + return nil, os.ErrInvalid + } + + root, err := p.q.FindRoot(ctx, segments[0]) + if err != nil { + return nil, os.ErrNotExist + } + + //TODO: Permissions checks + res, err := p.q.ResourceByPath(ctx, phylumsql.ResourceByPathParams{PathPrefix: p.prefix + "/", Search: segments, Root: root.ID}) + if err != nil { + return nil, os.ErrNotExist + } + + return &res, nil +} diff --git a/internal/phylumsql/db.go b/internal/phylumsql/db.go new file mode 100644 index 00000000..a4f044e5 --- /dev/null +++ b/internal/phylumsql/db.go @@ -0,0 +1,32 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package phylumsql + +import ( + "context" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" +) + +type DBTX interface { + Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error) + Query(context.Context, string, ...interface{}) (pgx.Rows, error) + QueryRow(context.Context, string, ...interface{}) pgx.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx pgx.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/internal/phylumsql/models.go b/internal/phylumsql/models.go new file mode 100644 index 00000000..abedb2ad --- /dev/null +++ b/internal/phylumsql/models.go @@ -0,0 +1,22 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 + +package phylumsql + +import ( + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +type Resource struct { + ID uuid.UUID + Parent *uuid.UUID + Name string + Dir bool + Created pgtype.Timestamp + Modified pgtype.Timestamp + Deleted pgtype.Timestamp + Size pgtype.Int4 + Etag pgtype.Text +} diff --git a/internal/phylumsql/resources.sql.go b/internal/phylumsql/resources.sql.go new file mode 100644 index 00000000..d1c7a952 --- /dev/null +++ b/internal/phylumsql/resources.sql.go @@ -0,0 +1,294 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.25.0 +// source: resources.sql + +package phylumsql + +import ( + "context" + + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" +) + +const createDirectory = `-- name: CreateDirectory :one +INSERT INTO resources( + id, parent, name, dir, created, modified +) VALUES ( + $1, $2, $3, true, NOW(), NOW() +) +RETURNING id, parent, name, dir, created, modified, deleted, size, etag +` + +type CreateDirectoryParams struct { + ID uuid.UUID + Parent *uuid.UUID + Name string +} + +func (q *Queries) CreateDirectory(ctx context.Context, arg CreateDirectoryParams) (Resource, error) { + row := q.db.QueryRow(ctx, createDirectory, arg.ID, arg.Parent, arg.Name) + var i Resource + err := row.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Deleted, + &i.Size, + &i.Etag, + ) + return i, err +} + +const createFile = `-- name: CreateFile :one +INSERT INTO resources( + id, parent, name, dir, created, modified, size, etag +) VALUES ( + $1, $2, $3, false, NOW(), NOW(), $4, $5 +) +RETURNING id, parent, name, dir, created, modified, deleted, size, etag +` + +type CreateFileParams struct { + ID uuid.UUID + Parent *uuid.UUID + Name string + Size pgtype.Int4 + Etag pgtype.Text +} + +func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (Resource, error) { + row := q.db.QueryRow(ctx, createFile, + arg.ID, + arg.Parent, + arg.Name, + arg.Size, + arg.Etag, + ) + var i Resource + err := row.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Deleted, + &i.Size, + &i.Etag, + ) + return i, err +} + +const deleteRecursive = `-- name: DeleteRecursive :exec +WITH RECURSIVE nodes(id, parent) AS ( + SELECT r.id, r.parent + FROM resources r WHERE r.id = $1::uuid + UNION ALL + SELECT r.id, r.parent + FROM resources r JOIN nodes n on r.parent = n.id + WHERE deleted IS NULL +) +UPDATE resources SET deleted = NOW() WHERE id in (SELECT id FROM nodes) +` + +func (q *Queries) DeleteRecursive(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteRecursive, id) + return err +} + +const findResource = `-- name: FindResource :one +SELECT id, parent, name, dir, created, modified, deleted, size, etag from resources WHERE id = $1 +` + +func (q *Queries) FindResource(ctx context.Context, id uuid.UUID) (Resource, error) { + row := q.db.QueryRow(ctx, findResource, id) + var i Resource + err := row.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Deleted, + &i.Size, + &i.Etag, + ) + return i, err +} + +const findRoot = `-- name: FindRoot :one +SELECT id, parent, name, dir, created, modified, deleted, size, etag from resources WHERE deleted IS NULL AND parent IS NULL AND name = $1 +` + +func (q *Queries) FindRoot(ctx context.Context, name string) (Resource, error) { + row := q.db.QueryRow(ctx, findRoot, name) + var i Resource + err := row.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Deleted, + &i.Size, + &i.Etag, + ) + return i, err +} + +const readDir = `-- name: ReadDir :many + +WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path) AS ( + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 1, concat($1::text || r.name)::text + FROM resources r WHERE r.parent = $2::uuid AND deleted IS NULL + UNION ALL + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, n.depth + 1, concat(n.path, '/', r.name) + FROM resources r JOIN nodes n on r.parent = n.id + WHERE deleted IS NULL + AND depth < $3::int +) +SELECT id, parent, name, dir, created, modified, size, etag, depth, path from nodes +` + +type ReadDirParams struct { + PathPrefix string + ID uuid.UUID + MaxDepth int32 +} + +type ReadDirRow struct { + ID uuid.UUID + Parent *uuid.UUID + Name string + Dir bool + Created pgtype.Timestamp + Modified pgtype.Timestamp + Size pgtype.Int4 + Etag pgtype.Text + Depth int32 + Path string +} + +// SELECT * from resources WHERE deleted IS NULL AND parent = $1; +func (q *Queries) ReadDir(ctx context.Context, arg ReadDirParams) ([]ReadDirRow, error) { + rows, err := q.db.Query(ctx, readDir, arg.PathPrefix, arg.ID, arg.MaxDepth) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReadDirRow + for rows.Next() { + var i ReadDirRow + if err := rows.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Size, + &i.Etag, + &i.Depth, + &i.Path, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const resourceByPath = `-- name: ResourceByPath :one +WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path, search) AS ( + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 0, concat($1::text, r.name)::text, $2::text[] + FROM resources r WHERE r.id = $3::uuid + UNION ALL + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, n.depth + 1, concat(n.path, '/', r.name), n.search + FROM resources r JOIN nodes n on r.parent = n.id + WHERE deleted IS NULL + AND r.name = n.search[n.depth + 2] +) +SELECT id, parent, name, dir, created, modified, size, etag, depth, path, search FROM nodes WHERE cardinality(search) = depth + 1 +` + +type ResourceByPathParams struct { + PathPrefix string + Search []string + Root uuid.UUID +} + +type ResourceByPathRow struct { + ID uuid.UUID + Parent *uuid.UUID + Name string + Dir bool + Created pgtype.Timestamp + Modified pgtype.Timestamp + Size pgtype.Int4 + Etag pgtype.Text + Depth int32 + Path string + Search []string +} + +func (q *Queries) ResourceByPath(ctx context.Context, arg ResourceByPathParams) (ResourceByPathRow, error) { + row := q.db.QueryRow(ctx, resourceByPath, arg.PathPrefix, arg.Search, arg.Root) + var i ResourceByPathRow + err := row.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Size, + &i.Etag, + &i.Depth, + &i.Path, + &i.Search, + ) + return i, err +} + +const updateResourceContents = `-- name: UpdateResourceContents :one +UPDATE resources +SET + size = $1, + etag = $2, + modified = NOW() +WHERE id = $3 +RETURNING id, parent, name, dir, created, modified, deleted, size, etag +` + +type UpdateResourceContentsParams struct { + Size pgtype.Int4 + Etag pgtype.Text + ID uuid.UUID +} + +func (q *Queries) UpdateResourceContents(ctx context.Context, arg UpdateResourceContentsParams) (Resource, error) { + row := q.db.QueryRow(ctx, updateResourceContents, arg.Size, arg.Etag, arg.ID) + var i Resource + err := row.Scan( + &i.ID, + &i.Parent, + &i.Name, + &i.Dir, + &i.Created, + &i.Modified, + &i.Deleted, + &i.Size, + &i.Etag, + ) + return i, err +} diff --git a/sql/migrations/001_resources.down.sql b/sql/migrations/001_resources.down.sql new file mode 100644 index 00000000..35bb99cf --- /dev/null +++ b/sql/migrations/001_resources.down.sql @@ -0,0 +1 @@ +DROP TABLE resources; \ No newline at end of file diff --git a/sql/migrations/001_resources.up.sql b/sql/migrations/001_resources.up.sql new file mode 100644 index 00000000..1954ab50 --- /dev/null +++ b/sql/migrations/001_resources.up.sql @@ -0,0 +1,14 @@ +CREATE TABLE resources ( + id uuid PRIMARY KEY, + parent uuid REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE, + name TEXT NOT NULL, + dir BOOLEAN NOT NULL, + created TIMESTAMP NOT NULL, + modified TIMESTAMP NOT NULL, + deleted TIMESTAMP, + size INT, + etag TEXT + -- CONSTRAINT resource_parent FOREIGN KEY REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE; +); + +CREATE UNIQUE INDEX unique_member_resource ON resources(parent, name) WHERE deleted IS NULL; \ No newline at end of file diff --git a/sql/queries/resources.sql b/sql/queries/resources.sql new file mode 100644 index 00000000..9de2de6f --- /dev/null +++ b/sql/queries/resources.sql @@ -0,0 +1,69 @@ +-- name: FindResource :one +SELECT * from resources WHERE id = $1; + +-- name: FindRoot :one +SELECT * from resources WHERE deleted IS NULL AND parent IS NULL AND name = $1; + +-- name: CreateDirectory :one +INSERT INTO resources( + id, parent, name, dir, created, modified +) VALUES ( + $1, $2, $3, true, NOW(), NOW() +) +RETURNING *; + +-- name: CreateFile :one +INSERT INTO resources( + id, parent, name, dir, created, modified, size, etag +) VALUES ( + $1, $2, $3, false, NOW(), NOW(), $4, $5 +) +RETURNING *; + +-- name: UpdateResourceContents :one +UPDATE resources +SET + size = $1, + etag = $2, + modified = NOW() +WHERE id = $3 +RETURNING *; + + +-- name: ReadDir :many +-- SELECT * from resources WHERE deleted IS NULL AND parent = $1; + +-- name: ReadDirRecursive :many +WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path) AS ( + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 1, concat(@path_prefix::text || r.name)::text + FROM resources r WHERE r.parent = @id::uuid AND deleted IS NULL + UNION ALL + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, n.depth + 1, concat(n.path, '/', r.name) + FROM resources r JOIN nodes n on r.parent = n.id + WHERE deleted IS NULL + AND depth < @max_depth::int +) +SELECT * from nodes; + +-- name: ResourceByPath :one +WITH RECURSIVE nodes(id, parent, name, dir, created, modified, size, etag, depth, path, search) AS ( + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, 0, concat(@path_prefix::text, r.name)::text, @search::text[] + FROM resources r WHERE r.id = @root::uuid + UNION ALL + SELECT r.id, r.parent, r.name, r.dir, r.created, r.modified, r.size, r.etag, n.depth + 1, concat(n.path, '/', r.name), n.search + FROM resources r JOIN nodes n on r.parent = n.id + WHERE deleted IS NULL + AND r.name = n.search[n.depth + 2] +) +SELECT * FROM nodes WHERE cardinality(search) = depth + 1; + +-- name: DeleteRecursive :exec +WITH RECURSIVE nodes(id, parent) AS ( + SELECT r.id, r.parent + FROM resources r WHERE r.id = @id::uuid + UNION ALL + SELECT r.id, r.parent + FROM resources r JOIN nodes n on r.parent = n.id + WHERE deleted IS NULL +) +UPDATE resources SET deleted = NOW() WHERE id in (SELECT id FROM nodes); \ No newline at end of file diff --git a/sqlc.yml b/sqlc.yml new file mode 100644 index 00000000..2cd3a19f --- /dev/null +++ b/sqlc.yml @@ -0,0 +1,21 @@ +version: "2" +sql: + - engine: "postgresql" + queries: "sql/queries" + schema: "sql/migrations" + gen: + go: + package: "phylumsql" + out: "phylumsql" + sql_package: "pgx/v5" + overrides: + - db_type: "uuid" + go_type: + import: "github.com/google/uuid" + type: "UUID" + - db_type: "uuid" + nullable: true + go_type: + import: "github.com/google/uuid" + type: "UUID" + pointer: true \ No newline at end of file