feat(uzsdk): 1, merge some variables to Configuration.
$ go run example.go
```
-
## Testing
module git.uzoombox.com/git/uzsdk
-go 1.20
+go 1.23
+
+toolchain go1.23.2
require (
- github.com/gin-gonic/gin v1.9.1
- github.com/jmoiron/sqlx v1.3.5
+ github.com/chromedp/cdproto v0.0.0-20241030022559-23c28aebe8cb
+ github.com/chromedp/chromedp v0.11.1
+ github.com/cstockton/go-conv v1.0.0
+ github.com/fatih/structs v1.1.0
+ github.com/gin-gonic/gin v1.10.0
+ github.com/jmoiron/sqlx v1.4.0
+ github.com/spaolacci/murmur3 v1.1.0
+ golang.org/x/sys v0.26.0
)
require (
- github.com/bytedance/sonic v1.9.1 // indirect
- github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
- github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/bytedance/sonic v1.12.3 // indirect
+ github.com/bytedance/sonic/loader v0.2.1 // indirect
+ github.com/chromedp/sysutil v1.1.0 // indirect
+ github.com/cloudwego/base64x v0.1.4 // indirect
+ github.com/cloudwego/iasm v0.2.0 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.6 // 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.14.0 // indirect
- github.com/goccy/go-json v0.10.2 // indirect
+ github.com/go-playground/validator/v10 v10.22.1 // indirect
+ github.com/gobwas/httphead v0.1.0 // indirect
+ github.com/gobwas/pool v0.2.1 // indirect
+ github.com/gobwas/ws v1.4.0 // indirect
+ github.com/goccy/go-json v0.10.3 // indirect
+ github.com/google/go-cmp v0.6.0 // indirect
+ github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/cpuid/v2 v2.2.4 // indirect
- github.com/leodido/go-urn v1.2.4 // indirect
- github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.8 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mailru/easyjson v0.7.7 // indirect
+ github.com/mattn/go-isatty v0.0.20 // 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.0.8 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
- github.com/ugorji/go/codec v1.2.11 // indirect
- golang.org/x/arch v0.3.0 // indirect
- golang.org/x/crypto v0.9.0 // indirect
- golang.org/x/net v0.10.0 // indirect
- golang.org/x/sys v0.8.0 // indirect
- golang.org/x/text v0.9.0 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
+ github.com/ugorji/go/codec v1.2.12 // indirect
+ golang.org/x/arch v0.11.0 // indirect
+ golang.org/x/crypto v0.28.0 // indirect
+ golang.org/x/net v0.30.0 // indirect
+ golang.org/x/text v0.19.0 // indirect
+ google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
-github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
-github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
-github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
-github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
-github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
-github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
+github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
+github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/chromedp/cdproto v0.0.0-20241030022559-23c28aebe8cb h1:yBPpAakATGLWZsVgYRcU9FopbOqzoazzbFaStQ9DCMc=
+github.com/chromedp/cdproto v0.0.0-20241030022559-23c28aebe8cb/go.mod h1:4XqMl3iIW08jtieURWL6Tt5924w21pxirC6th662XUM=
+github.com/chromedp/chromedp v0.11.1 h1:Spca8egFqUlv+JDW+yIs+ijlHlJDPufgrfXPwtq6NMs=
+github.com/chromedp/chromedp v0.11.1/go.mod h1:lr8dFRLKsdTTWb75C/Ttol2vnBKOSnt0BW8R9Xaupi8=
+github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
+github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
+github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
+github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/cstockton/go-conv v1.0.0 h1:zj/q/0MpQ/97XfiC9glWiohO8lhgR4TTnHYZifLTv6I=
+github.com/cstockton/go-conv v1.0.0/go.mod h1:HuiHkkRgOA0IoBNPC7ysG7kNpjDYlgM7Kj62yQPxjy4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
-github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
+github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
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/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
+github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
-github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
-github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
-github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
-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 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
+github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
+github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
+github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
+github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
+github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
+github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
+github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
+github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
+github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
+github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
+github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
-github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
+github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
+github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
-github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
-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/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
-github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-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/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
-github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
+github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
+github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
+github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
+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-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
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.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
-github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
+github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
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/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
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.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.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
-github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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=
-golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
-golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
-golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
-golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
-golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
-golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
+github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
+golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+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.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
-golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
-golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
-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.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
-google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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=
-rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package chrome
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/chromedp/cdproto/cdp"
+ "github.com/chromedp/chromedp"
+)
+
+// ClickandWait click an element with selector 'sel' if it's exist, and wait for done if 'wait' is true.
+func ClickandWait(ctx context.Context, sel string, options ...interface{}) error {
+ wait := true
+ for _, option := range options {
+ switch option.(type) {
+ case bool:
+ wait = option.(bool)
+ }
+ }
+ if err := TryWaiting(ctx, sel, time.Millisecond*10, 1); err == nil {
+ var data []byte
+ if err := chromedp.EvaluateAsDevTools(fmt.Sprintf(`document.querySelector('%v').click()`, sel), &data).Do(ctx); err != nil {
+ log.Println(err)
+ return err
+ } else {
+ if wait {
+ return chromedp.WaitNotPresent(sel, chromedp.ByQuery).Do(ctx)
+ } else {
+ return nil
+ }
+ }
+ } else {
+ return errors.New("element not found")
+ }
+}
+
+// FillInput fill an element with selector 'sel' using 'value'.
+func FillInput(ctx context.Context, sel, value string) error {
+ if err := chromedp.Focus(sel, chromedp.ByQuery).Do(ctx); err != nil {
+ return err
+ }
+ if err := chromedp.Clear(sel, chromedp.ByQuery).Do(ctx); err != nil {
+ return err
+ }
+ if err := chromedp.SendKeys(sel, value, chromedp.ByQuery).Do(ctx); err != nil {
+ return err
+ }
+ if false {
+ var val string
+ chromedp.Value(sel, &val, chromedp.ByQuery).Do(ctx)
+ }
+
+ return nil
+}
+
+// TryWaiting try to wait an element with selector 'sel' for exist.
+func TryWaiting(ctx context.Context, sel string, options ...interface{}) error {
+ interval := time.Millisecond * 20
+ count := 10
+ for _, option := range options {
+ switch option.(type) {
+ case time.Duration:
+ interval = option.(time.Duration)
+ case int:
+ count = option.(int)
+ }
+ }
+
+ var nodes []*cdp.Node
+ for i := 0; i < count; i++ {
+ if err := chromedp.Nodes(sel, &nodes, chromedp.AtLeast(0)).Do(ctx); err != nil {
+ log.Println(err)
+ return err
+ } else {
+ if len(nodes) > 0 {
+ return nil
+ }
+ }
+ chromedp.Sleep(interval).Do(ctx)
+ }
+
+ return errors.New("element not found")
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package convutil
+
+import (
+ "fmt"
+ "reflect"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/cstockton/go-conv"
+)
+
+// PrintStruct print information of struct, v can be ptr or struct.
+func PrintStruct(v interface{}) error {
+ vv := reflect.ValueOf(v)
+ if vv.Kind() == reflect.Ptr && !vv.IsNil() {
+ vv = vv.Elem()
+ }
+ if vv.Kind() != reflect.Struct {
+ return fmt.Errorf("v must be a valid pointer or struct object.")
+ }
+
+ for i := 0; i < vv.NumField(); i++ {
+ f := vv.Field(i)
+ if f.Kind() == reflect.Ptr {
+ f = f.Elem()
+ }
+ fmt.Printf("%v: %v\n", f.Kind(), f)
+ }
+
+ return nil
+}
+
+// AssignStruct set struct dst's value with map[string]interface{} src,
+// if force=true: assign value anytime,
+// else: assign value when field does not exist or field value is zero.
+func AssignStruct(dst interface{}, src interface{}, force bool) error {
+ vd := reflect.ValueOf(dst)
+ vs := reflect.ValueOf(src)
+ if vd.Kind() != reflect.Ptr || vd.IsNil() {
+ return fmt.Errorf("dst is not a valid pointer.")
+ }
+ if vs.Kind() != reflect.Ptr || vs.IsNil() {
+ return fmt.Errorf("src is not a valid pointer.")
+ }
+
+ vd = vd.Elem()
+ vs = vs.Elem()
+ if vd.Kind() != reflect.Struct {
+ return fmt.Errorf("dst must be a struct.")
+ }
+ if vs.Kind() != reflect.Map {
+ return fmt.Errorf("src must be a map.")
+ }
+
+ for _, k := range vs.MapKeys() {
+ key := k.String()
+ val := vs.MapIndex(k)
+ if f := vd.FieldByName(key); f.Kind() != reflect.Invalid {
+ assign := force
+ if !assign {
+ if f.Kind() == reflect.Interface || f.Kind() == reflect.Ptr {
+ fv := f.Elem()
+ if !fv.IsValid() || fv.IsZero() {
+ assign = true
+ }
+ }
+ }
+ if assign {
+ if err := convertAssign(f, val); err != nil {
+ return err
+ }
+ }
+ } else {
+ return fmt.Errorf("f[%s] not found.", key)
+ }
+ }
+
+ return nil
+}
+
+// CopyStruct make a copy of struct v,
+// if notnil=true: only copy fileds that value is not nil,
+// else: copy all fields even if value is nil.
+// options will be [excludes,includes]:
+//
+// excludes []string: MUST NOT copy fields.
+// includes []string: MUST copy fields that even it's value is nil.
+func CopyStruct(v interface{}, notnil bool, options ...interface{}) interface{} {
+ // parse options.
+ var excludes []string
+ var includes []string
+ for _, option := range options {
+ switch option.(type) {
+ case []string:
+ excludes = option.([]string)
+ case map[string]interface{}:
+ for k, v := range option.(map[string]interface{}) {
+ switch v.(type) {
+ case []string:
+ switch k {
+ case "excludes":
+ excludes = v.([]string)
+ case "includes":
+ includes = v.([]string)
+ }
+ }
+ }
+ }
+ }
+
+ var nullres struct{}
+ vv := reflect.ValueOf(v)
+ if vv.Kind() != reflect.Ptr || vv.IsNil() {
+ fmt.Println("v is not a valid pointer.")
+ return nullres
+ }
+ vv = vv.Elem()
+ if vv.Kind() != reflect.Struct {
+ fmt.Println("v must be a struct.")
+ return nullres
+ }
+
+ fields := make([]reflect.StructField, 0)
+ values := make([]reflect.Value, 0)
+ vt := reflect.TypeOf(vv.Interface())
+ for i := 0; i < vv.NumField(); i++ {
+ if value := vv.Field(i); value.Kind() != reflect.Invalid {
+ field := vt.Field(i)
+ need := true
+ must := false
+ for _, exclude := range excludes {
+ if exclude == field.Name {
+ need = false
+ break
+ }
+ }
+ for _, include := range includes {
+ if include == field.Name {
+ must = true
+ break
+ }
+ }
+ valid := value.IsValid()
+ switch value.Kind() {
+ case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
+ valid = !value.IsNil()
+ }
+ if (must) || (need && (valid || !notnil)) {
+ if 'A' <= field.Name[0] && 'Z' >= field.Name[0] { // fix: StructOf does not allow unexported fields
+ fields = append(fields, field)
+ values = append(values, value)
+ }
+ }
+ }
+ }
+ vd := reflect.New(reflect.StructOf(fields)).Elem()
+ for i := 0; i < len(fields); i++ {
+ vd.Field(i).Set(values[i])
+ }
+
+ return vd.Interface()
+}
+
+// BindString make a string from value,
+// and returns the point of it for binding.
+func BindString(value interface{}) *string {
+ val, _ := conv.String(value)
+ return &val
+}
+
+func convertAssign(dst, src reflect.Value) error {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return fmt.Errorf("src is not a pointer.")
+ }
+
+ switch dst.Interface().(type) {
+ case *bool:
+ if s, err := asBool(src); err == nil {
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *int:
+ if t, err := asInt(src); err == nil {
+ s := int(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *int8:
+ if t, err := asInt(src); err == nil {
+ s := int8(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *int16:
+ if t, err := asInt(src); err == nil {
+ s := int16(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *int32:
+ if t, err := asInt(src); err == nil {
+ s := int32(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *int64:
+ if s, err := asInt(src); err == nil {
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *uint:
+ if t, err := asInt(src); err == nil {
+ s := uint(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *uint8:
+ if t, err := asInt(src); err == nil {
+ s := uint8(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *uint16:
+ if t, err := asInt(src); err == nil {
+ s := uint16(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *uint32:
+ if t, err := asInt(src); err == nil {
+ s := uint32(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *uint64:
+ if t, err := asInt(src); err == nil {
+ s := uint64(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *float32:
+ if t, err := asFloat(src); err == nil {
+ s := float32(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *float64:
+ if s, err := asFloat(src); err == nil {
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *complex64:
+ if t, err := asComplex(src); err == nil {
+ s := complex64(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *complex128:
+ if s, err := asComplex(src); err == nil {
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *string:
+ if s, err := asString(src); err == nil {
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *[]byte:
+ if t, err := asString(src); err == nil {
+ s := []byte(t)
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ case *time.Time:
+ if s, err := asTime(src); err == nil {
+ dst.Set(reflect.ValueOf(&s))
+ return nil
+ }
+ }
+
+ return fmt.Errorf("unsupport data type: %#v=>%#v", src, dst)
+}
+
+func asBool(src reflect.Value) (bool, error) {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return false, fmt.Errorf("src must be Interface or Ptr")
+ }
+
+ switch src.Interface().(type) {
+ case bool:
+ return src.Elem().Bool(), nil
+ case int, int8, int16, int32, int64:
+ return src.Elem().Int() != 0, nil
+ case uint, uint8, uint16, uint32, uint64:
+ return src.Elem().Uint() != 0, nil
+ case string:
+ s := strings.ToUpper(strings.TrimSpace(src.Elem().String()))
+ return s == "TRUE" || s == "YES" || s == "Y", nil
+ case *bool:
+ s := *src.Interface().(*bool)
+ return s, nil
+ }
+
+ return false, fmt.Errorf("src cannot be converted")
+}
+
+func asInt(src reflect.Value) (int64, error) {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return 0, fmt.Errorf("src must be Interface or Ptr")
+ }
+
+ switch src.Interface().(type) {
+ case int, int8, int16, int32, int64:
+ return int64(src.Elem().Int()), nil
+ case uint, uint8, uint16, uint32, uint64:
+ return int64(src.Elem().Uint()), nil
+ case float32, float64:
+ return int64(src.Elem().Float()), nil
+ case complex64, complex128:
+ return int64(real(src.Elem().Complex())), nil
+ case string:
+ s, _ := strconv.ParseInt(src.Elem().String(), 0, 64)
+ return s, nil
+ case *int64:
+ s := *src.Interface().(*int64)
+ return s, nil
+ }
+
+ return 0, fmt.Errorf("src cannot be converted")
+}
+
+func asFloat(src reflect.Value) (float64, error) {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return 0, fmt.Errorf("src must be Interface or Ptr")
+ }
+
+ switch src.Interface().(type) {
+ case int, int8, int16, int32, int64:
+ return float64(src.Elem().Int()), nil
+ case uint, uint8, uint16, uint32, uint64:
+ return float64(src.Elem().Uint()), nil
+ case float32, float64:
+ return float64(src.Elem().Float()), nil
+ case complex64, complex128:
+ return real(src.Elem().Complex()), nil
+ case string:
+ s, _ := strconv.ParseFloat(src.Elem().String(), 64)
+ return s, nil
+ case *float64:
+ s := *src.Interface().(*float64)
+ return s, nil
+ }
+
+ return 0, fmt.Errorf("src cannot be converted")
+}
+
+func asComplex(src reflect.Value) (complex128, error) {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return 0, fmt.Errorf("src must be Interface or Ptr")
+ }
+
+ switch src.Interface().(type) {
+ case int, int8, int16, int32, int64:
+ return complex(float64(src.Elem().Int()), 0), nil
+ case uint, uint8, uint16, uint32, uint64:
+ return complex(float64(src.Elem().Uint()), 0), nil
+ case float32, float64:
+ return complex(float64(src.Elem().Float()), 0), nil
+ case complex64, complex128:
+ return src.Elem().Complex(), nil
+ case string:
+ s, _ := strconv.ParseFloat(src.Elem().String(), 64)
+ return complex(s, 0), nil
+ case *complex128:
+ s := *src.Interface().(*complex128)
+ return s, nil
+ }
+
+ return 0, fmt.Errorf("src cannot be converted")
+}
+
+func asString(src reflect.Value) (string, error) {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return "", fmt.Errorf("src must be Interface or Ptr")
+ }
+
+ switch src.Interface().(type) {
+ case int, int8, int16, int32, int64:
+ return fmt.Sprintf("%v", src.Elem().Int()), nil
+ case uint, uint8, uint16, uint32, uint64:
+ return fmt.Sprintf("%v", src.Elem().Uint()), nil
+ case float32, float64:
+ return fmt.Sprintf("%v", src.Elem().Float()), nil
+ case complex64, complex128:
+ return fmt.Sprintf("%v", src.Elem().Complex()), nil
+ case string:
+ return src.Elem().String(), nil
+ case []byte:
+ return string(src.Elem().Bytes()), nil
+ case *string:
+ s := *src.Interface().(*string)
+ return s, nil
+ }
+
+ return "", fmt.Errorf("src cannot be converted")
+}
+
+func asTime(src reflect.Value) (time.Time, error) {
+ if src.Kind() != reflect.Interface && src.Kind() != reflect.Ptr {
+ return time.Time{}, fmt.Errorf("src must be Interface or Ptr")
+ }
+
+ switch src.Interface().(type) {
+ case int, int8, int16, int32, int64:
+ return time.Unix(src.Elem().Int(), 0), nil
+ case uint, uint8, uint16, uint32, uint64:
+ return time.Unix(int64(src.Elem().Uint()), 0), nil
+ case string:
+ if s, err := time.Parse(time.RFC3339, src.Elem().String()); err == nil {
+ return s, nil
+ }
+ if s, err := time.Parse("2006-01-02 15:04:05", src.Elem().String()); err == nil {
+ return s, nil
+ }
+ if s, err := time.Parse("2006-01-02", src.Elem().String()); err == nil {
+ return s, nil
+ }
+ if s, err := time.Parse("15:04:05", src.Elem().String()); err == nil {
+ return s, nil
+ }
+ case time.Time:
+ return src.Elem().Interface().(time.Time), nil
+ case *time.Time:
+ s := *src.Interface().(*time.Time)
+ return s, nil
+ }
+
+ return time.Time{}, fmt.Errorf("src cannot be converted")
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package convutil
+
+import (
+ "reflect"
+ "sort"
+)
+
+// SortMap returns an sorted array of map keys.
+func SortMap(v interface{}) []string {
+ vv := reflect.ValueOf(v)
+ if vv.Kind() != reflect.Map {
+ return nil
+ }
+
+ keys := make([]string, 0)
+ for _, k := range vv.MapKeys() {
+ keys = append(keys, k.String())
+ }
+ return keys
+}
+
+// SortMapInt returns an sorted int array of map keys.
+func SortMapInt(v interface{}) []int {
+ vv := reflect.ValueOf(v)
+ if vv.Kind() != reflect.Map {
+ return nil
+ }
+ vks := vv.MapKeys()
+ if len(vks) <= 0 {
+ return nil
+ }
+
+ keys := make([]int, 0)
+ switch vks[0].Kind() {
+ case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+ for _, k := range vks {
+ keys = append(keys, int(k.Int()))
+ }
+ case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+ for _, k := range vks {
+ keys = append(keys, int(k.Uint()))
+ }
+ }
+
+ sort.Ints(keys)
+ return keys
+}
+
+// SortMapFloat64 returns an sorted float array of map keys.
+func SortMapFloat64(v interface{}) []float64 {
+ vv := reflect.ValueOf(v)
+ if vv.Kind() != reflect.Map {
+ return nil
+ }
+ vks := vv.MapKeys()
+ if len(vks) <= 0 {
+ return nil
+ }
+
+ keys := make([]float64, 0)
+ switch vks[0].Kind() {
+ case reflect.Float32, reflect.Float64:
+ for _, k := range vks {
+ keys = append(keys, k.Float())
+ }
+ }
+
+ sort.Float64s(keys)
+ return keys
+}
+
+// SortMapString returns an sorted string array of map keys.
+func SortMapString(v interface{}) []string {
+ vv := reflect.ValueOf(v)
+ if vv.Kind() != reflect.Map {
+ return nil
+ }
+ vks := vv.MapKeys()
+ if len(vks) <= 0 {
+ return nil
+ }
+
+ keys := make([]string, 0)
+ switch vks[0].Kind() {
+ case reflect.String:
+ for _, k := range vks {
+ keys = append(keys, k.String())
+ }
+ }
+
+ sort.Strings(keys)
+ return keys
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package dbutil
+
+import (
+ "fmt"
+ "regexp"
+ "strconv"
+ "strings"
+
+ "github.com/fatih/structs"
+)
+
+// FormatCtx define parameters for formatting query/stats/serie string.
+type FormatCtx struct {
+ TableName string
+ UseCount bool // for query
+ Wheres string // for stats
+}
+
+// Query defines request query object.
+type Query struct {
+ Fields []string
+ Wheres map[string]string
+ Groupby string
+ OrderBy string
+ Limits map[string]int64
+ Params map[string]string
+}
+
+// Parse returns request list to Query.
+func (query *Query) Parse(params interface{}) {
+ switch params.(type) {
+ case map[string]interface{}:
+ default:
+ return
+ }
+ for key, value := range params.(map[string]interface{}) {
+ switch strings.ToLower(key) {
+ case "fields":
+ query.Fields = make([]string, 0)
+ switch value.(type) {
+ case string:
+ query.Fields = append(query.Fields, value.(string))
+ case []interface{}:
+ for _, v := range value.([]interface{}) {
+ switch v.(type) {
+ case string:
+ query.Fields = append(query.Fields, v.(string))
+ }
+ }
+ }
+ case "wheres":
+ query.Wheres = make(map[string]string)
+ switch value.(type) {
+ case map[string]interface{}:
+ for k, v := range value.(map[string]interface{}) {
+ switch v.(type) {
+ case string:
+ query.Wheres[k] = fmt.Sprintf("%s", v.(string))
+ case []interface{}:
+ vt := make([]string, 0)
+ for _, t := range v.([]interface{}) {
+ switch t.(type) {
+ case string:
+ vt = append(vt, fmt.Sprintf("'%s'", t.(string)))
+ }
+ }
+ if len(vt) >= 2 {
+ query.Wheres[k] = fmt.Sprintf("BETWEEN %s AND %s", vt[0], vt[1])
+ }
+ case map[string]interface{}:
+ for _, t := range v.(map[string]interface{}) {
+ switch t.(type) {
+ case []interface{}:
+ vt := make([]string, 0)
+ for _, p := range t.([]interface{}) {
+ switch p.(type) {
+ case string:
+ vt = append(vt, fmt.Sprintf("'%s'", p.(string)))
+ }
+ }
+ if len(vt) > 0 {
+ query.Wheres[k] = fmt.Sprintf("IN(%s)", strings.Join(vt, ","))
+ }
+ }
+ break
+ }
+ }
+ }
+ }
+ case "groupby":
+ switch value.(type) {
+ case string:
+ query.Groupby = value.(string)
+ }
+ case "orderby":
+ switch value.(type) {
+ case string:
+ query.OrderBy = value.(string)
+ }
+ case "limits":
+ query.Limits = make(map[string]int64)
+ switch value.(type) {
+ case map[string]interface{}:
+ for k, v := range value.(map[string]interface{}) {
+ switch strings.ToLower(k) {
+ case "offset":
+ query.Limits["offset"], _ = strconv.ParseInt(fmt.Sprint(v), 0, 64)
+ case "limit":
+ query.Limits["limit"], _ = strconv.ParseInt(fmt.Sprint(v), 0, 64)
+ }
+ }
+ }
+ case "params":
+ query.Params = make(map[string]string)
+ switch value.(type) {
+ case map[string]interface{}:
+ for k, v := range value.(map[string]interface{}) {
+ query.Params[k] = fmt.Sprint(v)
+ }
+ }
+ }
+ }
+}
+
+// Format return formatted query string using table name
+func (query *Query) Format(ctx FormatCtx) string {
+ fields := "*"
+ if len(query.Fields) > 0 {
+ fields = strings.Join(query.Fields, ",")
+ }
+ wheres := ""
+ conditions := make([]string, 0)
+ if query.Wheres != nil {
+ for key := range query.Wheres {
+ conditions = append(conditions, fmt.Sprintf("%s %s", key, query.Wheres[key]))
+ }
+ }
+ if ctx.Wheres != "" {
+ conditions = append(conditions, ctx.Wheres)
+ }
+ if len(conditions) > 0 {
+ wheres = fmt.Sprintf(" WHERE %s", strings.Join(conditions, " AND "))
+ }
+ groupby := ""
+ if query.Groupby != "" {
+ groupby = fmt.Sprintf(" GROUP BY %s ", query.Groupby)
+ }
+ orderby := ""
+ if query.OrderBy != "" {
+ orderby = fmt.Sprintf(" ORDER BY %s", query.OrderBy)
+ }
+ limits := ""
+ if query.Limits["offset"] > 0 {
+ limits += fmt.Sprintf(" OFFSET %d", query.Limits["offset"])
+ }
+ if query.Limits["limit"] > 0 {
+ limits += fmt.Sprintf(" LIMIT %d", query.Limits["limit"])
+ }
+
+ var sql string
+ if ctx.UseCount {
+ sql = "SELECT COUNT(1) FROM " + ctx.TableName + wheres
+ } else {
+ sql = "SELECT " + fields + " FROM " + ctx.TableName + wheres + groupby + orderby + limits
+ }
+ return sql
+}
+
+// Stats defines request stats object.
+type Stats struct {
+ Name string
+ Function string
+ Timezone string
+ Series []string
+ Wheres map[string]string
+}
+
+// StatsRes defines response stats object.
+type StatsRes struct {
+ Minute *interface{} `db:"minute"`
+ Hour *interface{} `db:"hour"`
+ Day *interface{} `db:"day"`
+ Week *interface{} `db:"week"`
+ Month *interface{} `db:"month"`
+ Quarter *interface{} `db:"quarter"`
+ Year *interface{} `db:"year"`
+ All *interface{} `db:"all"`
+}
+
+// Parse returns request list to Stats.
+func (stats *Stats) Parse(params interface{}) {
+ switch params.(type) {
+ case map[string]interface{}:
+ default:
+ return
+ }
+
+ for key, value := range params.(map[string]interface{}) {
+ switch strings.ToLower(key) {
+ case "name":
+ stats.Name = fmt.Sprint(value)
+ case "function":
+ stats.Function = strings.ToLower(fmt.Sprint(value))
+ case "timezone":
+ stats.Timezone = fmt.Sprint(value)
+ case "series":
+ switch value.(type) {
+ case string:
+ stats.Series = append(stats.Series, value.(string))
+ case []interface{}:
+ for _, v := range value.([]interface{}) {
+ switch v.(type) {
+ case string:
+ stats.Series = append(stats.Series, v.(string))
+ }
+ }
+ }
+ case "wheres":
+ stats.Wheres = make(map[string]string)
+ switch value.(type) {
+ case map[string]interface{}:
+ for k, v := range value.(map[string]interface{}) {
+ switch v.(type) {
+ case string:
+ stats.Wheres[k] = fmt.Sprintf("%s", v.(string))
+ case []interface{}:
+ vt := make([]string, 0)
+ for _, t := range v.([]interface{}) {
+ switch t.(type) {
+ case string:
+ vt = append(vt, fmt.Sprintf("'%s'", t.(string)))
+ }
+ }
+ if len(vt) >= 2 {
+ stats.Wheres[k] = fmt.Sprintf("BETWEEN %s AND %s", vt[0], vt[1])
+ }
+ case map[string]interface{}:
+ for _, t := range v.(map[string]interface{}) {
+ switch t.(type) {
+ case []interface{}:
+ vt := make([]string, 0)
+ for _, p := range t.([]interface{}) {
+ switch p.(type) {
+ case string:
+ vt = append(vt, fmt.Sprintf("'%s'", p.(string)))
+ }
+ }
+ if len(vt) > 0 {
+ stats.Wheres[k] = fmt.Sprintf("IN(%s)", strings.Join(vt, ","))
+ }
+ }
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if stats.Function == "count" {
+ stats.Name = "1"
+ }
+ if stats.Timezone == "" {
+ stats.Timezone = "+8" // UTC+8
+ }
+}
+
+// Format return formatted stats string using table name
+func (stats *Stats) Format(ctx FormatCtx) string {
+ if len(stats.Series) <= 0 {
+ return ""
+ }
+ wheres := ""
+ conditions := make([]string, 0)
+ if stats.Wheres != nil {
+ for key := range stats.Wheres {
+ conditions = append(conditions, fmt.Sprintf("%s %s", key, stats.Wheres[key]))
+ }
+ }
+ if ctx.Wheres != "" {
+ conditions = append(conditions, ctx.Wheres)
+ }
+ if len(conditions) > 0 {
+ wheres = fmt.Sprintf(" WHERE %s", strings.Join(conditions, " AND "))
+ }
+
+ var sql string
+ var template string
+ template = fmt.Sprintf("SELECT %v(%v) FROM %v %v", stats.Function, stats.Name, ctx.TableName, wheres)
+ totals := make([]string, 0)
+ for _, v := range stats.Series {
+ key := strings.ToLower(v)
+ switch key {
+ case "minute", "hour", "day", "week", "month", "quarter", "year":
+ totals = append(totals, fmt.Sprintf("(%v AND (created AT TIME ZONE '%v')>=date_trunc('%s', LOCALTIMESTAMP AT TIME ZONE '%v')) AS %s", template, stats.Timezone, key, stats.Timezone, v))
+ case "all":
+ totals = append(totals, fmt.Sprintf("(%v) AS %s", template, v))
+ }
+ }
+ sql = "SELECT "
+ for i, v := range totals {
+ if i > 0 {
+ sql += ","
+ }
+ sql += v
+ }
+ return sql
+}
+
+// Serie defines request serie object.
+type Serie struct {
+ Name string
+ Function string
+ Timezone string
+ Start string
+ Stop string
+ Step string
+ Wheres map[string]string
+}
+
+// SerieRes defines response serie object.
+type SerieRes struct {
+ Field []interface{}
+ Value []interface{}
+}
+
+// Parse returns request list to Stats.
+func (serie *Serie) Parse(params interface{}) {
+ switch params.(type) {
+ case map[string]interface{}:
+ default:
+ return
+ }
+
+ for key, value := range params.(map[string]interface{}) {
+ switch strings.ToLower(key) {
+ case "name":
+ serie.Name = fmt.Sprint(value)
+ case "function":
+ serie.Function = strings.ToLower(fmt.Sprint(value))
+ case "timezone":
+ serie.Timezone = fmt.Sprint(value)
+ case "start":
+ serie.Start = fmt.Sprint(value)
+ case "stop":
+ serie.Stop = fmt.Sprint(value)
+ case "step":
+ serie.Step = fmt.Sprint(value)
+ case "wheres":
+ serie.Wheres = make(map[string]string)
+ switch value.(type) {
+ case map[string]interface{}:
+ for k, v := range value.(map[string]interface{}) {
+ switch v.(type) {
+ case string:
+ serie.Wheres[k] = fmt.Sprintf("%s", v.(string))
+ case []interface{}:
+ vt := make([]string, 0)
+ for _, t := range v.([]interface{}) {
+ switch t.(type) {
+ case string:
+ vt = append(vt, fmt.Sprintf("'%s'", t.(string)))
+ }
+ }
+ if len(vt) >= 2 {
+ serie.Wheres[k] = fmt.Sprintf("BETWEEN %s AND %s", vt[0], vt[1])
+ }
+ case map[string]interface{}:
+ for _, t := range v.(map[string]interface{}) {
+ switch t.(type) {
+ case []interface{}:
+ vt := make([]string, 0)
+ for _, p := range t.([]interface{}) {
+ switch p.(type) {
+ case string:
+ vt = append(vt, fmt.Sprintf("'%s'", p.(string)))
+ }
+ }
+ if len(vt) > 0 {
+ serie.Wheres[k] = fmt.Sprintf("IN(%s)", strings.Join(vt, ","))
+ }
+ }
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if serie.Function == "count" {
+ serie.Name = "1"
+ }
+ if serie.Timezone == "" {
+ serie.Timezone = "+8" // UTC+8
+ }
+}
+
+// Format return formatted stats string using table name
+func (serie *Serie) Format(ctx FormatCtx) string {
+ if len(serie.Step) <= 0 {
+ return ""
+ }
+ wheres := ""
+ conditions := make([]string, 0)
+ if serie.Wheres != nil {
+ for key := range serie.Wheres {
+ conditions = append(conditions, fmt.Sprintf("%s %s", key, serie.Wheres[key]))
+ }
+ }
+ if ctx.Wheres != "" {
+ conditions = append(conditions, ctx.Wheres)
+ }
+ if len(conditions) > 0 {
+ wheres = fmt.Sprintf(" WHERE %s", strings.Join(conditions, " AND "))
+ }
+
+ re := regexp.MustCompile(`(^[0-9 ]*|[ s]*)`)
+ key := re.ReplaceAllString(strings.ToLower(serie.Step), "")
+ num := 10
+ format := "YYYY"
+ switch key {
+ case "minute":
+ num = 59
+ format = "YYYY-MM-DD HH:MI"
+ case "hour":
+ num = 23
+ format = "YYYY-MM-DD HH:00"
+ case "day":
+ num = 29
+ format = "YYYY-MM-DD"
+ case "week":
+ num = 53
+ format = "YYYY-WW"
+ case "month":
+ num = 11
+ format = "YYYY-MM"
+ case "quarter":
+ num = 33
+ format = "YYYY-Q"
+ key = "month"
+ serie.Step = "3 month"
+ case "year":
+ num = 9
+ format = "YYYY"
+ }
+ format = fmt.Sprintf("'%v'", format)
+ if serie.Start == "" {
+ serie.Start = fmt.Sprintf("LOCALTIMESTAMP-interval '%d %s'", num, key)
+ } else {
+ serie.Start = fmt.Sprintf("TIMESTAMP '%v'", serie.Start)
+ }
+ serie.Start = fmt.Sprintf("(%v) AT TIME ZONE '%v'", serie.Start, serie.Timezone)
+ if serie.Stop == "" {
+ serie.Stop = "LOCALTIMESTAMP"
+ } else {
+ serie.Stop = fmt.Sprintf("TIMESTAMP '%v'", serie.Stop)
+ }
+ serie.Stop = fmt.Sprintf("(%v) AT TIME ZONE '%v'", serie.Stop, serie.Timezone)
+ if serie.Step[0] != '\'' {
+ serie.Step = fmt.Sprintf("'%v'", serie.Step)
+ }
+
+ sql := fmt.Sprintf(`
+SELECT x.v AS key, COALESCE(y.v, 0) AS value FROM (
+ SELECT to_char(s.t, %v) AS v FROM (
+ SELECT generate_series(%v, %v, %v)
+ ) AS s(t))
+ AS x
+LEFT JOIN (
+ SELECT to_char(created AT TIME ZONE '%v', %v) AS k, %v(%v) AS v FROM %v %v GROUP BY k
+) AS y ON x.v = y.k;
+`, format, serie.Start, serie.Stop, serie.Step, serie.Timezone, format, serie.Function, serie.Name, ctx.TableName, wheres)
+ return sql
+}
+
+// Wheres returns an formatted sql where string without 'WHERE' from obj's value,
+// exp: {ad_client_id: '1', name: 'test'} => "ad_client_id='1' AND name='test'"
+func Wheres(obj interface{}) string {
+ var wheres string
+ m := make(map[string]interface{})
+ switch obj.(type) {
+ case map[string]interface{}:
+ m = obj.(map[string]interface{})
+ default:
+ m = structs.Map(obj)
+ }
+ conditions := make([]string, 0)
+ for key, value := range m {
+ conditions = append(conditions, fmt.Sprintf("%v=%v", key, value))
+ }
+ if len(conditions) > 0 {
+ wheres = strings.Join(conditions, " AND ")
+ }
+
+ return wheres
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package dsutil
+
+import (
+ "reflect"
+)
+
+// Equal returns true if array equals to val.
+func Equal(array interface{}, val interface{}) bool {
+ va, vv := reflect.ValueOf(array), reflect.ValueOf(val)
+ if (va.Kind() != reflect.Array && va.Kind() != reflect.Slice) ||
+ (vv.Kind() != reflect.Array && vv.Kind() != reflect.Slice) {
+ return false
+ }
+
+ na, nv := va.Len(), vv.Len()
+ if na != nv {
+ return false
+ }
+
+ i := 0
+ for i = 0; i < na; i++ {
+ if va.Index(i).Interface() != vv.Index(i).Interface() {
+ break
+ }
+ }
+ return i == na
+}
+
+// IncludesAt returns index if array includes val.
+func IncludesAt(array interface{}, val interface{}) int {
+ va, vv := reflect.ValueOf(array), reflect.ValueOf(val)
+ if (va.Kind() != reflect.Array && va.Kind() != reflect.Slice) || 0 >= va.Len() {
+ return -1
+ }
+
+ if vv.Kind() == reflect.Array || vv.Kind() == reflect.Slice {
+ if 0 >= vv.Len() || reflect.TypeOf(va.Index(0).Interface()) != reflect.TypeOf(vv.Index(0).Interface()) {
+ return -1
+ }
+
+ alen, vlen := va.Len(), vv.Len()
+ var i, j int
+ for i = 0; i <= alen-vlen; i++ {
+ for j = 0; j < vlen; j++ {
+ if va.Index(i+j).Interface() != vv.Index(j).Interface() {
+ break
+ }
+ }
+ if j == vlen {
+ return i
+ }
+ }
+ } else {
+ if reflect.TypeOf(va.Index(0).Interface()) != reflect.TypeOf(val) {
+ return -1
+ }
+
+ for i := 0; i < va.Len(); i++ {
+ if va.Index(i).Interface() == val {
+ return i
+ }
+ }
+ }
+
+ return -1
+}
+
+// ContainsAny returns true if array contains any elements in val.
+func ContainsAny(array interface{}, val interface{}) bool {
+ i, _ := ContainsAnyAt(array, val)
+ return i != -1
+}
+
+// ContainsAnyAt returns index if array contains any elements in val.
+func ContainsAnyAt(array interface{}, val interface{}) (int, int) {
+ va, vv := reflect.ValueOf(array), reflect.ValueOf(val)
+ if (va.Kind() != reflect.Array && va.Kind() != reflect.Slice) || 0 >= va.Len() {
+ return -1, -1
+ }
+
+ if vv.Kind() == reflect.Array || vv.Kind() == reflect.Slice {
+ if 0 >= vv.Len() || reflect.TypeOf(va.Index(0).Interface()) != reflect.TypeOf(vv.Index(0).Interface()) {
+ return -1, -1
+ }
+
+ for i := 0; i < va.Len(); i++ {
+ for j := 0; j < vv.Len(); j++ {
+ if va.Index(i).Interface() == vv.Index(j).Interface() {
+ return i, j
+ }
+ }
+ }
+ } else {
+ if reflect.TypeOf(va.Index(0).Interface()) != reflect.TypeOf(val) {
+ return -1, -1
+ }
+
+ for i := 0; i < va.Len(); i++ {
+ if va.Index(i).Interface() == val {
+ return i, 0
+ }
+ }
+ }
+
+ return -1, -1
+}
+
+// ContainsAll returns true if array contains all elements in val.
+func ContainsAll(array interface{}, val interface{}) bool {
+ res := ContainsAllAt(array, val)
+ return res != nil
+}
+
+// ContainsAllAt returns index if array contains all elements in val.
+func ContainsAllAt(array interface{}, val interface{}) []int {
+ va, vv := reflect.ValueOf(array), reflect.ValueOf(val)
+ if (va.Kind() != reflect.Array && va.Kind() != reflect.Slice) || 0 >= va.Len() {
+ return nil
+ }
+
+ if vv.Kind() == reflect.Array || vv.Kind() == reflect.Slice {
+ if 0 >= vv.Len() || reflect.TypeOf(va.Index(0).Interface()) != reflect.TypeOf(vv.Index(0).Interface()) {
+ return nil
+ }
+
+ alen, vlen := va.Len(), vv.Len()
+ used, res := []bool{}, []int{}
+ for i := 0; i < alen; i++ {
+ used = append(used, false)
+ }
+ for i := 0; i < vlen; i++ {
+ res = append(res, -1)
+ }
+
+ var i, j int
+ for i = 0; i < vlen; i++ {
+ for j = 0; j < alen; j++ {
+ if used[j] {
+ continue
+ }
+ if va.Index(j).Interface() == vv.Index(i).Interface() {
+ used[j] = true
+ res[i] = j
+ break
+ }
+ }
+ if j == alen {
+ return nil
+ }
+ }
+ if i == vlen {
+ return res
+ }
+ } else {
+ if reflect.TypeOf(va.Index(0).Interface()) != reflect.TypeOf(val) {
+ return nil
+ }
+
+ for i := 0; i < va.Len(); i++ {
+ if va.Index(i).Interface() == val {
+ return []int{i}
+ }
+ }
+ }
+ return nil
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package fmtutil
+
+import (
+ "fmt"
+ "math"
+ "strconv"
+ "time"
+)
+
+// FormatDate returns a string formatted value with type: date.storage.
+func FormatDate(value interface{}, timezone, format string) string {
+ var s time.Time
+ switch value.(type) {
+ case string:
+ s, _ = time.Parse(time.RFC3339, value.(string))
+ case *time.Time:
+ s = *value.(*time.Time)
+ }
+
+ var options string
+ switch format {
+ case "date":
+ options = "2006/01/02"
+ case "time":
+ options = "15:04:05"
+ default:
+ options = "2006/01/02 15:04:05"
+ }
+
+ if timezone == "" {
+ timezone = "Asia/Shanghai"
+ }
+ tz, _ := time.LoadLocation(timezone)
+ return s.In(tz).Format(options)
+}
+
+// FormatStorage returns a string formatted value with type: uom.storage.
+func FormatStorage(value interface{}) string {
+ var bytes int64
+ switch value.(type) {
+ case int64:
+ bytes = value.(int64)
+ case *string:
+ bytes, _ = strconv.ParseInt(*value.(*string), 0, 64)
+ }
+
+ if bytes == 0 {
+ return "0 B"
+ }
+ units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
+ i := math.Floor(math.Log(float64(bytes)) / math.Log(1024))
+ return fmt.Sprintf("%.2f %v", (float64(bytes) / math.Pow(1024, i)), units[int(i)])
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package fsutil
+
+import (
+ "archive/zip"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "git.uzoombox.com/git/uzsdk/utils/osutil"
+)
+
+// ZipFilepath use zip to archive a path(s) and output to zipfile,
+// if successful, it return nil, else return an error.
+// options will be [recursive, files]:
+//
+// recursive bool: zip directory recursively, default: true.
+// files []string: only zip these files, default zip all files.
+func ZipFilepath(paths []string, target string, options ...interface{}) error {
+ var recursive = true
+ var files []string
+ for _, option := range options {
+ switch option.(type) {
+ case bool:
+ recursive = option.(bool)
+ case []string:
+ files = option.([]string)
+ }
+ }
+
+ zf, err := os.Create(target)
+ if err != nil {
+ return err
+ }
+ defer zf.Close()
+
+ zw := zip.NewWriter(zf)
+ defer zw.Close()
+
+ if len(files) > 0 { // only zip those file that found in 'files'.
+ for _, file := range files {
+ var path string
+ for i := 0; i < len(paths); i++ {
+ if strings.HasPrefix(file, paths[i]) && len(paths[i]) > len(path) {
+ path = paths[i]
+ }
+ }
+ if osutil.IsDir(file) {
+ filepath.WalkDir(file, func(p string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if !recursive { // do not zip recursively.
+ if d.IsDir() && file != p {
+ return filepath.SkipDir
+ }
+ }
+ return zipFilenode(zw, path, p)
+ })
+ } else {
+ zipFilenode(zw, path, file)
+ }
+ }
+ } else { // zip all files.
+ for _, path := range paths {
+ filepath.WalkDir(path, func(p string, d fs.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ if !recursive { // do not zip recursively.
+ if d.IsDir() && path != p {
+ return filepath.SkipDir
+ }
+ }
+ return zipFilenode(zw, path, p)
+ })
+ }
+ }
+
+ return nil
+}
+
+// UnzipFilepath use zip to unarchive a path and output to target directory,
+// if successful, it return nil, else return an error.
+func UnzipFilepath(path, target string) error {
+ zr, err := zip.OpenReader(path)
+ if err != nil {
+ return err
+ }
+ defer zr.Close()
+
+ root, err := filepath.Abs(target)
+ if err != nil {
+ return err
+ }
+
+ for _, zf := range zr.File {
+ if err = unzipFilenode(zr, root, zf); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func zipFilenode(w *zip.Writer, root, path string) error {
+ // create a local file header.
+ fi, err := os.Stat(path)
+ if err != nil {
+ return err
+ }
+ fh, err := zip.FileInfoHeader(fi)
+ if err != nil {
+ return err
+ }
+ fh.Method = zip.Deflate
+ fh.Name, err = filepath.Rel(filepath.Dir(root), path)
+ if err != nil {
+ return err
+ }
+ if fi.IsDir() {
+ fh.Name += "/"
+ }
+
+ // create writer for this file header.
+ fw, err := w.CreateHeader(fh)
+ if err != nil {
+ return err
+ }
+
+ if fi.IsDir() {
+ return nil
+ }
+
+ // save content of the file.
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ _, err = io.Copy(fw, f)
+ return err
+}
+
+func unzipFilenode(_ *zip.ReadCloser, root string, zf *zip.File) error {
+ path := filepath.Join(root, zf.Name)
+ if !strings.HasPrefix(path, filepath.Clean(root)+string(os.PathSeparator)) {
+ return fmt.Errorf("invalid file path: %s", path)
+ }
+
+ if zf.FileInfo().IsDir() {
+ if err := os.MkdirAll(path, 0755); err != nil {
+ return err
+ }
+ return nil
+ }
+
+ // create directory tree.
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+
+ // create a local file for receiving unzipped content.
+ f, err := os.Create(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ // unzip the content of zipfile and copy to local file.
+ fr, err := zf.Open()
+ if err != nil {
+ return err
+ }
+ defer fr.Close()
+
+ _, err = io.Copy(f, fr)
+ return err
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package fsutil
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/md5"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "sort"
+)
+
+// Md5sum returns the MD5 checksum of file path.
+func Md5sum(path string) (sum string) {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return
+ }
+
+ if fi.IsDir() {
+ if false {
+ names := make([]string, 0)
+ filepath.WalkDir(path, func(p string, d os.DirEntry, err error) error {
+ if err != nil {
+ return err
+ }
+ names = append(names, p)
+ if d.IsDir() && path != p {
+ return filepath.SkipDir
+ }
+ return nil
+ })
+
+ sort.Strings(names)
+ sum = Md5string(names...)
+ } else {
+ sum = ""
+ }
+ } else {
+ sum = Md5file(path)
+ }
+
+ return
+}
+
+// Md5file returns the MD5 checksum of a file.
+func Md5file(path string) string {
+ var sum string
+ if f, err := os.Open(path); err == nil {
+ h := md5.New()
+ if _, err := io.Copy(h, f); err == nil {
+ sum = fmt.Sprintf("%x", h.Sum(nil))
+ }
+ f.Close()
+ }
+ return sum
+}
+
+// Md5string returns the MD5 checksum of string(s).
+func Md5string(ss ...string) string {
+ h := md5.New()
+ for _, s := range ss {
+ io.WriteString(h, s)
+ }
+ return fmt.Sprintf("%x", h.Sum(nil))
+}
+
+// AesEncrypt use keyval to encrypt data with AES, and returns the result.
+func AesEncrypt(data, keyval []byte) []byte {
+ var dst []byte
+ key := Aes256Key(keyval)
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return dst
+ }
+
+ defer func() {
+ if err := recover(); err != nil {
+ fmt.Println("panic occurred:", err)
+ }
+ }()
+
+ blocksize := block.BlockSize()
+ padding := blocksize - len(data)%blocksize
+ src := append(data, bytes.Repeat([]byte{byte(padding)}, padding)...) // pkcs#7
+ dst = make([]byte, len(src))
+ mode := cipher.NewCBCEncrypter(block, key[:blocksize]) // iv=key[:blocksize]
+ mode.CryptBlocks(dst, src)
+ return dst
+}
+
+// AesDecrypt use keyval to decrypt data with AES, and returns the result.
+func AesDecrypt(data, keyval []byte) []byte {
+ var dst []byte
+ key := Aes256Key(keyval)
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return dst
+ }
+
+ defer func() {
+ if err := recover(); err != nil {
+ fmt.Println("panic occurred:", err)
+ }
+ }()
+
+ blocksize := block.BlockSize()
+ dstlen := len(data)
+ dst = make([]byte, dstlen)
+ mode := cipher.NewCBCDecrypter(block, key[:blocksize])
+ mode.CryptBlocks(dst, data)
+ unpadding := int(dst[dstlen-1])
+ return dst[:(dstlen - unpadding)] // pkcs#7
+}
+
+// AesCryptfile use keyval to encrypt/decrypt file with AES, and returns the result.
+func AesCryptfile(src, dst string, keyval []byte) error {
+ in, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer in.Close()
+ out, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0644)
+ if err != nil {
+ return err
+ }
+ defer out.Close()
+
+ key := Aes256Key(keyval)
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if err := recover(); err != nil {
+ fmt.Println("panic occurred:", err)
+ }
+ }()
+
+ blocksize := block.BlockSize()
+ stream := cipher.NewOFB(block, key[:blocksize]) // iv=key[:blocksize]
+ reader := &cipher.StreamReader{S: stream, R: in}
+ _, err = io.Copy(out, reader)
+ return err
+}
+
+// Aes256Key returns a key with 256-bits used for aes from val.
+func Aes256Key(val []byte) []byte {
+ keylen := 32
+ key := make([]byte, 0)
+ namelen := len(val)
+ for len(key) < keylen {
+ left := keylen - len(key)
+ addlen := left
+ if left > namelen {
+ addlen = namelen
+ }
+ key = append(key, []byte(val[:addlen])...)
+ }
+ return key
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package fsutil
+
+import (
+ "encoding/base64"
+ "log"
+ "os"
+)
+
+// Base64file returns the base64 encoded string of a file.
+func Base64file(path string) string {
+ var result string
+ if bytes, err := os.ReadFile(path); err == nil {
+ result = base64.StdEncoding.EncodeToString(bytes)
+ }
+ return result
+}
+
+// Base64Encode returns the base64 encoded string of a text,
+// options will be [noPadding]:
+//
+// noPadding bool: not padding with '=' if set to true, default: false.
+func Base64Encode(text string, options ...interface{}) string {
+ var noPadding bool
+ for _, option := range options {
+ switch option.(type) {
+ case bool:
+ noPadding = option.(bool)
+ }
+ }
+
+ if noPadding {
+ return base64.RawStdEncoding.EncodeToString([]byte(text))
+ } else {
+ return base64.StdEncoding.EncodeToString([]byte(text))
+ }
+}
+
+// Base64Decode returns the decoded string with base64 of a text,
+// options will be [noPadding]:
+//
+// noPadding bool: not padding with '=' if set to true, default: false.
+func Base64Decode(text string, options ...interface{}) string {
+ var noPadding bool
+ for _, option := range options {
+ switch option.(type) {
+ case bool:
+ noPadding = option.(bool)
+ }
+ }
+
+ buf, err := []byte{}, error(nil)
+ if noPadding {
+ buf, err = base64.RawStdEncoding.DecodeString(text)
+ } else {
+ buf, err = base64.StdEncoding.DecodeString(text)
+ }
+ if err != nil {
+ log.Println(err)
+ }
+
+ return string(buf)
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package fsutil
+
+import (
+ "os"
+ "regexp"
+ "strings"
+)
+
+// Filetype returns filetype by mimetype like:[
+//
+// document, presentation, spreadsheet, package, pdf,
+// audio, image, text, video, chemical, font, message, model, application,
+// folder, httpd, other]
+//
+// read more from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+func Filetype(name string) string {
+ types := strings.Split(name, "/")
+ if len(types) > 1 {
+ switch types[0] {
+ case "application":
+ {
+ if matched, _ := regexp.MatchString(`(ms[-]?word)|(wordprocessingml)|(opendocument\.text)|(abiword)`, types[1]); matched {
+ return "document"
+ } else if matched, _ := regexp.MatchString(`(ms[-]?powerpoint)|(presentationml)|(opendocument\.presentation)`, types[1]); matched {
+ return "presentation"
+ } else if matched, _ := regexp.MatchString(`(ms[-]?excel)|(spreadsheetml)|(opendocument\.spreadsheet)`, types[1]); matched {
+ return "spreadsheet"
+ } else if matched, _ := regexp.MatchString(`((archive|compressed|cd-image|diskimage|freearc|rar|tar|zip2?)$)|(\.installer)`, types[1]); matched {
+ return "package"
+ } else if matched, _ := regexp.MatchString(`^pdf$`, types[1]); matched {
+ return "pdf"
+ } else if matched, _ := regexp.MatchString(`(^(\w+script|(ld\+)?json|(xhtml\+)?xml|rtf|sql|x-c?sh|x-php|x-subrip|x-yaml)$)|((\.xul\+)+xml$)`, types[1]); matched {
+ return "text"
+ } else if matched, _ := regexp.MatchString(`fontobject$`, types[1]); matched {
+ return "font"
+ } else if matched, _ := regexp.MatchString(`^ogg$`, types[1]); matched {
+ return "video"
+ } else {
+ return "application"
+ }
+ }
+ case "audio":
+ return "audio"
+ case "chemical":
+ return "chemical"
+ case "font":
+ return "font"
+ case "httpd":
+ {
+ if types[1] == "unix-directory" {
+ return "folder"
+ } else {
+ return "httpd"
+ }
+ }
+ case "image":
+ return "image"
+ case "message":
+ return "message"
+ case "model":
+ return "model"
+ case "text":
+ return "text"
+ case "video":
+ return "video"
+ }
+ }
+
+ return "other"
+}
+
+// Filemime returns regexp string of mimetype by filetype, exp:
+//
+// document => (ms[-]?word)|(wordprocessingml)|(opendocument\.text)|(abiword)
+//
+// read more from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
+func Filemime(name string) string {
+ switch strings.ToLower(name) {
+ case "document":
+ return `^application/.+((ms[-]?word)|(wordprocessingml)|(opendocument\.text)|(abiword))`
+ case "presentation":
+ return `^application/.+((ms[-]?powerpoint)|(presentationml)|(opendocument\.presentation))`
+ case "spreadsheet":
+ return `^application/.+((ms[-]?excel)|(spreadsheetml)|(opendocument\.spreadsheet))`
+ case "package":
+ return `^application/.*(((archive|compressed|cd-image|diskimage|freearc|rar|tar|zip2?)$)|(\.installer))`
+ case "pdf":
+ return `^application/pdf$`
+ case "application":
+ return `^application/`
+ case "audio":
+ return `^audio/`
+ case "chemical":
+ return `^chemical/`
+ case "font":
+ return `^application/.*(fontobject$)` + `|` + `^font/`
+ case "folder":
+ return `^httpd/unix-directory`
+ case "httpd":
+ return `^httpd/(?!unix-directory).`
+ case "image":
+ return `^image/`
+ case "message":
+ return `^message/`
+ case "model":
+ return `^model/`
+ case "text":
+ return `^application/.*(((\w+script|(ld\+)?json|(xhtml\+)?xml|rtf|sql|x-c?sh|x-php|x-subrip|x-yaml)$)|((\.xul\+)+xml$))` + `|` + `^text/`
+ case "video":
+ return `^application/(ogg$)` + `|` + `^video/`
+ case "other":
+ return `^(?!application|audio|chemical|font|httpd|image|message|model|text|video).`
+ }
+
+ return `^.{0}$`
+}
+
+// Filesize returns filesize of the regular file(following symbolic link).
+func Filesize(path string) int64 {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return -1
+ }
+
+ return fi.Size()
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package netutil
+
+import (
+ "io/fs"
+ "mime"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+)
+
+// Mimetype returns the MIME type associated with the file path,
+// if path is a directory, returns "httpd/unix-directory".
+// Parameters WOULD include [fi:string/fs.FileInfo, re:*regexp.Regexp]
+func Mimetype(options ...interface{}) string {
+ var fi fs.FileInfo
+ var re *regexp.Regexp
+ for _, option := range options {
+ switch option.(type) {
+ case string:
+ fi, _ = os.Stat(option.(string))
+ case fs.FileInfo:
+ fi = option.(fs.FileInfo)
+ case *regexp.Regexp:
+ re = option.(*regexp.Regexp)
+ }
+ }
+
+ if fi.IsDir() {
+ return "httpd/unix-directory"
+ }
+ path := fi.Name()
+ if re != nil {
+ if loc := re.FindStringIndex(path); loc != nil {
+ path = path[:loc[0]]
+ }
+ }
+ return mime.TypeByExtension(filepath.Ext(path))
+}
+
+// Multipart returns a multipart string that split from Header.Content-Type,
+// options will be [content]:
+//
+// content [*http.Request, *string]: header["Content-Type"]
+func Multipart(options ...interface{}) string {
+ var content string // "Content-Type: multipart/form-data;boundary=---yb1zYhTI38xpQxBK"
+ for _, option := range options {
+ switch option.(type) {
+ case *http.Request:
+ content = option.(*http.Request).Header.Get("Content-Type")
+ case *string:
+ content = *option.(*string)
+ }
+ }
+
+ for _, part := range strings.Split(content, ";") {
+ if strings.Contains(part, "multipart") {
+ return part
+ }
+ }
+ return ""
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package osutil
+
+import (
+ "io"
+ "os/exec"
+)
+
+// ExecCommand construct a command struct and then call cmd.Start and cmd.Wait,
+// returns the command struct and exec status at last.
+func ExecCommand(name string, arg ...string) (*exec.Cmd, error) {
+ cmd := exec.Command(name, arg...)
+ err := cmd.Start()
+ if err != nil {
+ return cmd, err
+ }
+
+ return cmd, cmd.Wait()
+}
+
+// ReadCommandOutput returns command's exec status, stdout and stderr buffer.
+func ReadCommandOutput(name string, arg ...string) (err error, obuf, ebuf []byte) {
+ cmd := exec.Command(name, arg...)
+ o, err := cmd.StdoutPipe()
+ if err != nil {
+ return err, nil, nil
+ }
+ e, err := cmd.StderrPipe()
+ if err != nil {
+ return err, nil, nil
+ }
+
+ err = cmd.Start()
+ if err != nil {
+ return err, nil, nil
+ }
+ obuf, err = io.ReadAll(o)
+ ebuf, err = io.ReadAll(e)
+
+ cmd.Wait()
+ return err, obuf, ebuf
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package osutil
+
+import (
+ "io/fs"
+ "log"
+ "os"
+ "path/filepath"
+ "regexp"
+)
+
+// GetAppDirectory returns directory where the application located, NOT working directory!!!
+// Use os.Getwd to get current working directory.
+func GetAppDirectory() string {
+ dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
+ if err != nil {
+ log.Fatal(err)
+ }
+ return dir
+}
+
+// CreateDirectory creates a directory named path, along with any necessary parents.
+func CreateDirectory(path string) error {
+ return os.MkdirAll(path, 0755)
+}
+
+// IsPathExist returns true if the path(file or directory) exist.
+func IsPathExist(path string) bool {
+ if _, err := os.Stat(path); err != nil {
+ if os.IsNotExist(err) {
+ return false
+ }
+ }
+ return true
+}
+
+// IsDir returns true if the path is an exist directory.
+func IsDir(path string) bool {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ return fi.IsDir()
+}
+
+// IsNotDir returns true if the path is not a directory(not exist or not a directory).
+func IsNotDir(path string) bool {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return true
+ }
+ return !fi.IsDir()
+}
+
+// IsRegular returns true if the path is an exist regular file.
+func IsRegular(path string) bool {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ return fi.Mode().IsRegular()
+}
+
+// IsNotRegular returns true if the path is not a regular file(not exist or not a regular file).
+func IsNotRegular(path string) bool {
+ fi, err := os.Stat(path)
+ if err != nil {
+ return true
+ }
+ return !fi.Mode().IsRegular()
+}
+
+// IsSymlink returns true if the path is a symbolic link.
+func IsSymlink(path string) bool {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return false
+ }
+ return (fi.Mode() & fs.ModeSymlink) != 0
+}
+
+// IsNotSymlink returns true if the path is not a symbolic link(not exist or not a symbolic link).
+func IsNotSymlink(path string) bool {
+ fi, err := os.Lstat(path)
+ if err != nil {
+ return true
+ }
+ return (fi.Mode() & fs.ModeSymlink) == 0
+}
+
+// SearchFile returns an array of filepath that match.
+func SearchFile(root string, pattern string) []string {
+ result := make([]string, 0)
+ re := regexp.MustCompile(pattern)
+ filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if info.Mode().IsRegular() && re.MatchString(info.Name()) {
+ result = append(result, path)
+ }
+ return nil
+ })
+
+ return result
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package osutil
+
+import (
+ "fmt"
+ "math"
+
+ "golang.org/x/sys/unix"
+)
+
+// DiskUsage estimate file space usage, returns the command struct and exec status at last.
+func DiskUsage(path string) (map[string]interface{}, error) {
+ result := make(map[string]interface{})
+ var buf unix.Statfs_t
+ if err := unix.Statfs(path, &buf); err != nil {
+ return result, err
+ }
+
+ total := buf.Blocks * uint64(buf.Bsize)
+ free := buf.Bfree * uint64(buf.Bsize)
+ result["total"] = total
+ result["free"] = free
+ return result, nil
+}
+
+// FormatStorage formats storage from a bytes to a human-readable string.
+func FormatStorage(bytes uint64) string {
+ if bytes == 0 {
+ return "0 B"
+ }
+ units := []string{"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"}
+ n := math.Floor(math.Log(float64(bytes)) / math.Log(1024))
+ i := int(n)
+ if 0 <= i && len(units) > i {
+ return fmt.Sprintf("%.2f %s", float64(bytes)/math.Pow(1024, n), units[i])
+ }
+ return "nil"
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package strutil
+
+import (
+ "encoding/base64"
+ "fmt"
+ "log"
+
+ "github.com/spaolacci/murmur3"
+)
+
+// Base64URLEncode returns a base64 encoded string of v.
+func Base64URLEncode(v string) string {
+ return base64.RawURLEncoding.EncodeToString([]byte(v))
+}
+
+// Base64URLDecode returns a decoded string with base64 of v.
+func Base64URLDecode(v string) string {
+ buf, err := base64.RawURLEncoding.DecodeString(v)
+ if err != nil {
+ log.Println(err)
+ }
+ return string(buf)
+}
+
+// Base64URLHash returns a base64 encoded string hashed with murmur of v.
+func Base64URLHash(v interface{}) string {
+ str := fmt.Sprint(v)
+ sum := murmur3.Sum64([]byte(str))
+ buf := fmt.Sprintf("%.16x", sum)
+ return Base64URLEncode(buf)
+}
--- /dev/null
+// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+package strutil
+
+import (
+ "bytes"
+)
+
+var JSONHTMLRules = []map[string]string{
+ {
+ "\\\"": "\"",
+ },
+ {
+ "\\\"": "\"",
+ "\\\\n": "\\n",
+ },
+ {
+ "\\\\u0026": "&",
+ "\\\\u003c": "<",
+ "\\\\u003e": ">",
+ },
+}
+
+var HTMLRules = map[string]string{
+ "\"": """,
+ "&": "&",
+ "<": "<",
+ ">": ">",
+}
+
+var XMLRules = map[string]string{
+ "'": "'",
+ "\"": """,
+ "&": "&",
+ "<": "<",
+ ">": ">",
+}
+
+// ReplaceAll returns a copy of the slice s with the rules that: replace name with value if reverse is false, else replae value with name.
+func ReplaceAll(s []byte, rules map[string]string, reverse bool) []byte {
+ if !reverse {
+ for name, value := range rules {
+ s = bytes.ReplaceAll(s, []byte(name), []byte(value))
+ }
+ } else {
+ for name, value := range rules {
+ s = bytes.ReplaceAll(s, []byte(value), []byte(name))
+ }
+ }
+
+ return s
+}
+
+// JSONHTMLUnescape returns a copy of the slice s with JSONHTMLRules.
+func JSONHTMLUnescape(s []byte) []byte {
+ for i := 0; i < len(JSONHTMLRules); i++ {
+ s = ReplaceAll(s, JSONHTMLRules[i], false)
+ }
+ return s
+}
+
+// HTMLEscape returns a copy of the slice s with HTMLRules.
+func HTMLEscape(s []byte) []byte {
+ return ReplaceAll(s, HTMLRules, false)
+}
+
+// HTMLUnescape returns a copy of the slice s with HTMLRules.
+func HTMLUnescape(s []byte) []byte {
+ return ReplaceAll(s, HTMLRules, true)
+}
+
+// XMLEscape returns a copy of the slice s with XMLRules.
+func XMLEscape(s []byte) []byte {
+ return ReplaceAll(s, XMLRules, false)
+}
+
+// XMLUnescape returns a copy of the slice s with XMLRules.
+func XMLUnescape(s []byte) []byte {
+ return ReplaceAll(s, XMLRules, true)
+}
--- /dev/null
+// Copyright 2010-2023 nanzoom.com. All Rights Reserved.
+
+package strutil
+
+import (
+ "math"
+ "strings"
+)
+
+// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
+func Index(s, substr string) int {
+ r, subrunes := []rune(s), []rune(substr)
+ rlen, subrlen := len(r), len(subrunes)
+ for i := 0; i <= rlen-subrlen; i++ {
+ if string(r[i:i+subrlen]) == substr {
+ return i
+ }
+ }
+ return -1
+}
+
+// Indexof returns the index of the first instance of substr in s[i:], or -1 if substr is not present in s[i:].
+func Indexof(s, substr string, i int) int {
+ if i > len(s)-1 {
+ return -1
+ }
+ if i <= 0 {
+ return strings.Index(s, substr)
+ }
+ ind := strings.Index(s[i:], substr)
+ if ind == -1 {
+ return -1
+ }
+ return ind + i
+}
+
+// LastIndexof returns the index of the last instance of substr in s[i:], or -1 if substr is not present in s[i:].
+func LastIndexof(s, substr string, i int) int {
+ if i < 0 {
+ return -1
+ }
+ if i >= len(s) {
+ return strings.LastIndex(s, substr)
+ }
+ ind := strings.LastIndex(s[i:], substr)
+ if ind == -1 {
+ return -1
+ }
+ return ind + 1
+}
+
+// IndexofRune returns the index of the first instance of substr in s[i:], or -1 if substr is not present in s[i:].
+func IndexofRune(s, substr []rune, i int) int {
+ if i > len(s)-1 {
+ return -1
+ }
+ if i <= 0 {
+ return IndexRune(s, substr)
+ }
+ ind := IndexRune(s[i:], substr)
+ if ind == -1 {
+ return -1
+ }
+ return ind + i
+}
+
+// LastIndexofRune returns the index of the last instance of substr in s[i:], or -1 if substr is not present in s[i:].
+func LastIndexofRune(s, substr []rune, i int) int {
+ if i < 0 {
+ return -1
+ }
+ if i >= len(s) {
+ return LastIndexRune(s, substr)
+ }
+ ind := LastIndexRune(s[i:], substr)
+ if ind == -1 {
+ return -1
+ }
+ return ind + 1
+}
+
+// CompareRune is the rune version of strings.Compare.
+func CompareRune(a, b []rune) int {
+ if len(a) < len(b) {
+ return -1
+ }
+ if len(a) > len(b) {
+ return 1
+ }
+
+ for i, c := range a {
+ if c < b[i] {
+ return -1
+ }
+ if c > b[i] {
+ return 1
+ }
+ }
+
+ return 0
+}
+
+// IndexRune is the rune version of strings.Index.
+func IndexRune(s, substr []rune) int {
+ need := len(substr)
+ last := len(s) - need
+ for i := 0; i <= last; i++ {
+ if 0 == CompareRune(s[i:i+need], substr) {
+ return i
+ }
+ }
+ return -1
+}
+
+// LastIndexRune is the rune version of strings.LastIndex.
+func LastIndexRune(s, substr []rune) int {
+ need := len(substr)
+ last := len(s) - need
+ for i := last; i >= 0; i-- {
+ if 0 == CompareRune(s[i:i+need], substr) {
+ return i
+ }
+ }
+ return -1
+}
+
+// CommonLength returns equal length of two texts,
+// which start from head when prefix is true, else from tail.
+func CommonLength(a, b string, prefix bool) int {
+ return CommonLengthRune([]rune(a), []rune(b), prefix)
+}
+
+// CommonLengthRune returns equal length of two runes,
+// which start from head when prefix is true, else from tail.
+func CommonLengthRune(a, b []rune, prefix bool) int {
+ if prefix {
+ n := 0
+ for ; n < len(a) && n < len(b); n++ {
+ if a[n] != b[n] {
+ return n
+ }
+ }
+ return n
+ } else {
+ i1 := len(a)
+ i2 := len(b)
+ for n := 0; ; n++ {
+ i1--
+ i2--
+ if i1 < 0 || i2 < 0 || a[i1] != b[i2] {
+ return n
+ }
+ }
+ }
+}
+
+// CommonOverlap determines if the suffix of one string is the prefix of another.
+func CommonOverlap(text1 string, text2 string) int {
+ // Cache the text lengths to prevent multiple calls.
+ text1Length := len(text1)
+ text2Length := len(text2)
+ // Eliminate the null case.
+ if text1Length == 0 || text2Length == 0 {
+ return 0
+ }
+ // Truncate the longer string.
+ if text1Length > text2Length {
+ text1 = text1[text1Length-text2Length:]
+ } else if text1Length < text2Length {
+ text2 = text2[0:text1Length]
+ }
+ textLength := int(math.Min(float64(text1Length), float64(text2Length)))
+ // Quick check for the worst case.
+ if text1 == text2 {
+ return textLength
+ }
+
+ // Start by looking for a single character match and increase length until no match is found. Performance analysis: http://neil.fraser.name/news/2010/11/04/
+ best := 0
+ length := 1
+ for {
+ pattern := text1[textLength-length:]
+ found := strings.Index(text2, pattern)
+ if found == -1 {
+ break
+ }
+ length += found
+ if found == 0 || text1[textLength-length:] == text2[0:length] {
+ best = length
+ length++
+ }
+ }
+
+ return best
+}
--- /dev/null
+#!/bin/bash
+# Author: bruce(bruce@nanzoom.com)
+# Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+
+DEBUG="yes"
+SHELL=`basename $0`
+CURDIR=`pwd`
+
+#----------------------------------------------------------
+# generateSbs(path, output):
+# generate code for creating Sbs.
+# paremeter:
+# path: generate from which, default: ../service/struct.go
+# output: output filename, default: stdout
+#----------------------------------------------------------
+function generateSbs() {
+ # parse parameters.
+ path="$CURDIR/service/struct.go"; if [ $# -ge 1 ]; then
+ path=$1
+ fi
+ output=/dev/stdout; if [ $# -ge 2 ]; then
+ output=$2
+ fi
+ if [ $DEBUG == "yes" ]; then
+ echo "input($*), output(path:$path,output:$output)"
+ fi
+
+ # prepare variables.
+
+ # execute.
+ IFS=$'\n'
+ echo "map[string]*sqlbuilder.Struct{" >$output
+ for line in `cat $path |grep -E "^type\s+\w+\s+struct\s+{\w*$"`; do
+ name=`echo $line |awk '{print $2}'`
+ echo " \"$name\": sqlbuilder.NewStruct(new($name)).For(sqlbuilder.PostgreSQL)," >>$output
+ done
+ echo "}" >>$output
+}
+
+#----------------------------------------------------------
+# generateStruct(dbname, username, output):
+# generate code for table structures.
+# paremeter:
+# dbname: database name to connect to.
+# username: database user name.
+# output: output filename.
+#----------------------------------------------------------
+function generateStruct() {
+ # parse parameters.
+ dbname="serverdb"; if [ $# -ge 1 ]; then
+ dbname=$1
+ fi
+ username="postgres"; if [ $# -ge 2 ]; then
+ username=$2
+ fi
+ output="struct.txt"; if [ $# -ge 3 ]; then
+ output=$3
+ fi
+ if [ $DEBUG == "yes" ]; then
+ echo "input($*), output(dbname:$dbname,username:$username,output:$output)"
+ fi
+
+ # prepare variables.
+ sqlfile="$CURDIR/db.sql"
+
+ # execute.
+ IFS=$'\n'
+ echo -e "// this file is created by utils.sh.generateStruct.\n\n" >$output
+ echo "\t" >$sqlfile; echo "\dt" >>$sqlfile
+ for name in `psql -U $username -d $dbname -f $sqlfile |awk -F "|" '{print $2}'`; do
+ name=`echo $name |sed 's/^[ \t]*//g' |sed 's/[ \t]*$//g'`
+ exportTableDict $name
+ done
+ rm -f $sqlfile
+}
+
+function exportTableDict() {
+ local name=$1
+ local sqlfile="$CURDIR/temp.sql"
+ local tmpfile="$CURDIR/temp.txt"
+
+ pascal=`echo $name |sed --expression 's/_\([a-z]\)/\U\1/g' --expression 's/^\(.\)/\U\1/g'`
+ echo "// $pascal is table struct." >>$output
+ echo "type $pascal struct {" >>$output
+ echo "\t" >$sqlfile; echo "\d $name" >>$sqlfile
+ for row in `psql -U $username -d $dbname -f $sqlfile |grep -v "Tuples only"`; do
+ column=`echo $row |awk -F "|" '{print $1}' |sed 's/^[ \t]*//g' |sed 's/[ \t]*$//g'`
+ type=`echo $row |awk -F "|" '{print $2}' |sed 's/^[ \t]*//g' |sed 's/[ \t]*$//g'`
+ nullable=`echo $row |awk -F "|" '{print $4}' |sed 's/^[ \t]*//g' |sed 's/[ \t]*$//g'`; if [ "$nullable"=="not null" ]; then
+ nullable="Y"
+ fi
+ default=`echo $row |awk -F "|" '{print $5}' |sed 's/^[ \t]*//g' |sed 's/[ \t]*$//g'`
+ pascal=`echo $column |sed --expression 's/_\([a-z]\)/\U\1/g' --expression 's/^\(.\)/\U\1/g'`
+ typestr="*string"
+ istime=`echo $type |grep "timestamp"`; if [ -n "$istime" ]; then
+ typestr="*time.Time"
+ fi
+ isserial=`echo $default |grep -e "^nextval"`
+
+ if [ -n "$isserial" ]; then
+ echo " $pascal $typestr \`db:\"$column\" fieldopt:\"omitempty\"\`" >>$output
+ else
+ echo " $pascal $typestr \`db:\"$column\"\`" >>$output
+ fi
+ done
+ echo -e "}\n" >>$output
+ rm -f $sqlfile $tmpfile
+}
+
+# generateSbs "$@"
+generateStruct "$@"
\ No newline at end of file
-// Copyright 2023 nanzoom.com. All Rights Reserved.
+// Copyright 2010-2024 nanzoom.com. All Rights Reserved.
package uzsdk
// Application defines structure of application's information.
type Application struct {
NAME string
+ UUID string
VENDOR string
AUTHOR string
VERSION string
-// Copyright 2010-2022 nanzoom.com. All Rights Reserved.
+// Copyright 2010-2024 nanzoom.com. All Rights Reserved.
package uzsdk
DataDirectory string // server data directory, default: /data/cloud
TrustedProxies []string // a list of proxies trusted, default: ["127.0.0.1"]
}
+ System struct {
+ DBDriver string // database driver, default: postgres
+ DBHost string // database host, default: 127.0.0.1
+ DBPort int64 // database port, default: 5432
+ DBUser string // database user, default: postgres
+ DBPassword string // database password, default: postgres
+ DBName string // database name, default: serverdb
+ Heartbeat int64 // heartbeat(unit: second) for keepalive between client and server, default: 36000
+ IdleTime int64 // server kick off client those do nothing after this idle time, default: [Heartbeat]<<2
+ CheckTime int64 // interval that server check client status, default: [Heartbeat]>>1
+ DbgOutput bool // debug: output more details, default: fasle.
+ DbgToken bool // debug: use token '63886e35-7291-4b4d-a66f-fa7431542ace', default: false
+ }
+ GZip struct {
+ Enable bool // enable gzip with gin, default: true
+ Level int64 // gzip compress level, default: gzip.DefaultCompression
+ }
+ Logging struct {
+ Level string // TODO: log level, default: error
+ Prefix string // prefix add to each log message, default: ""
+ File struct {
+ Name string // log filename
+ }
+ Syslog struct {
+ Enable bool // TODO: enable log to system, default: false
+ }
+ }
}
-// Copyright 2023 nanzoom.com. All Rights Reserved.
+// Copyright 2010-2024 nanzoom.com. All Rights Reserved.
package uzsdk
// Instance is the framework's instance, used for server & plugin both.
type Instance struct {
+ Application *Application // application of this application.
Conf *Configuration // configuration of this application.
Db *sqlx.DB // the first database connection created by main routine.
Logger *logger.Logger // logger created by main routine.
-// Copyright 2023 nanzoom.com. All Rights Reserved.
-/* ## Package versions
-go 1.20
-
-require (
- git.uzoombox.com/git/uzsdk v0.1.0
- github.com/cavaliergopher/grab/v3 v3.0.1
- github.com/chromedp/cdproto v0.0.0-20230611221135-4cd95c996604
- github.com/chromedp/chromedp v0.9.1
- github.com/cstockton/go-conv v1.0.0
- github.com/fatih/structs v1.1.0
- github.com/gin-contrib/gzip v0.0.6
- github.com/gin-gonic/gin v1.9.1
- github.com/huandu/go-sqlbuilder v1.21.0
- github.com/jmoiron/sqlx v1.3.5
- github.com/json-iterator/go v1.1.12
- github.com/lib/pq v1.10.9
- github.com/mafredri/cdp v0.34.1
- github.com/pdfcpu/pdfcpu v0.4.1
- github.com/spaolacci/murmur3 v1.1.0
- github.com/wechatpay-apiv3/wechatpay-go v0.2.16
- golang.org/x/sys v0.8.0
-)
-
-require (
- github.com/bytedance/sonic v1.9.1 // indirect
- github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
- github.com/chromedp/sysutil v1.0.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.14.0 // indirect
- github.com/gobwas/httphead v0.1.0 // indirect
- github.com/gobwas/pool v0.2.1 // indirect
- github.com/gobwas/ws v1.1.0 // indirect
- github.com/goccy/go-json v0.10.2 // indirect
- github.com/gorilla/websocket v1.4.2 // indirect
- github.com/hhrutter/lzw v1.0.0 // indirect
- github.com/hhrutter/tiff v1.0.0 // indirect
- github.com/huandu/xstrings v1.3.2 // indirect
- github.com/josharian/intern v1.0.0 // indirect
- github.com/klauspost/cpuid/v2 v2.2.4 // indirect
- github.com/leodido/go-urn v1.2.4 // indirect
- github.com/mailru/easyjson v0.7.7 // indirect
- github.com/mattn/go-isatty v0.0.19 // indirect
- github.com/mattn/go-runewidth v0.0.14 // 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.0.8 // indirect
- github.com/pkg/errors v0.9.1 // indirect
- github.com/rivo/uniseg v0.4.4 // indirect
- github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
- github.com/ugorji/go/codec v1.2.11 // indirect
- golang.org/x/arch v0.3.0 // indirect
- golang.org/x/crypto v0.9.0 // indirect
- golang.org/x/image v0.5.0 // indirect
- golang.org/x/net v0.10.0 // indirect
- golang.org/x/text v0.9.0 // indirect
- google.golang.org/protobuf v1.30.0 // indirect
- gopkg.in/yaml.v2 v2.4.0 // indirect
- gopkg.in/yaml.v3 v3.0.1 // indirect
-)
-*/
-
+// Copyright 2010-2024 nanzoom.com. All Rights Reserved.
package uzsdk
// Version is the current opensdk's version.
-const Version = "0.2.5"
+const Version = "0.3.0"