mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-31 09:00:34 -06:00
enhancement: introduce theme processing (#9133)
* enhancement: introduce theme processing * enhancement: introduce theme processing * enhancement: add theme processing tests and changelog * Update services/web/pkg/config/config.go Co-authored-by: Michael Barz <michael.barz@zeitgestalten.eu> * fix: ci findings * Apply suggestions from code review Co-authored-by: Martin <github@diemattels.at> * enhancement: use the theme assets from web instead of having them inside the oCis repo (license clash Apache vs. AGPLv3) * fix: golangci tagalign order * fix: rename UnifiedRoleUploader to UnifiedRoleEditorLite * fix: some typos Co-authored-by: Michael Barz <michael.barz@zeitgestalten.eu> * enhancement: export supported theme logo upload filetypes * chore: bump reva * fix: allow init func --------- Co-authored-by: Michael Barz <michael.barz@zeitgestalten.eu> Co-authored-by: Martin <github@diemattels.at>
This commit is contained in:
@@ -48,10 +48,16 @@ linters-settings:
|
||||
align: false
|
||||
sort: true
|
||||
order:
|
||||
- json
|
||||
- yaml
|
||||
- mapstructure
|
||||
- env
|
||||
- desc
|
||||
- introductionVersion
|
||||
- deprecationVersion
|
||||
- removalVersion
|
||||
- deprecationInfo
|
||||
- deprecationReplacement
|
||||
gocyclo:
|
||||
min-complexity: 35 # there are some func unfortunately who need this - should we refactor them?
|
||||
gomoddirectives:
|
||||
|
||||
15
changelog/unreleased/enhancement-theme-processing.md
Normal file
15
changelog/unreleased/enhancement-theme-processing.md
Normal file
@@ -0,0 +1,15 @@
|
||||
Enhancement: Theme Processing and Logo Customization
|
||||
|
||||
We have made significant improvements to the theme processing in Infinite Scale.
|
||||
The changes include:
|
||||
|
||||
- Enhanced the way themes are composed. Now, the final theme is a combination of the built-in theme and the custom theme provided by the administrator via `WEB_ASSET_THEMES_PATH` and `WEB_UI_THEME_PATH`.
|
||||
- Introduced a new mechanism to load custom assets. This is particularly useful when a single asset, such as a logo, needs to be overwritten.
|
||||
- Fixed the logo customization option. Previously, small theme changes would copy the entire theme. Now, only the changed keys are considered, making the process more efficient.
|
||||
- Default themes are now part of ocis. This change simplifies the theme management process for web.
|
||||
|
||||
These changes enhance the robustness of the theme handling in Infinite Scale and provide a better user experience.
|
||||
|
||||
|
||||
https://github.com/owncloud/ocis/pull/9133
|
||||
https://github.com/owncloud/ocis/issues/8966
|
||||
3
go.mod
3
go.mod
@@ -15,7 +15,7 @@ require (
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible
|
||||
github.com/coreos/go-oidc/v3 v3.10.0
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e
|
||||
github.com/dhowden/tag v0.0.0-20230630033851-978a0926ee25
|
||||
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e
|
||||
github.com/egirna/icap-client v0.1.1
|
||||
@@ -88,6 +88,7 @@ require (
|
||||
github.com/test-go/testify v1.1.4
|
||||
github.com/thejerf/suture/v4 v4.0.5
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
github.com/tus/tusd v1.13.0
|
||||
github.com/unrolled/secure v1.14.0
|
||||
github.com/urfave/cli/v2 v2.27.2
|
||||
|
||||
11
go.sum
11
go.sum
@@ -1025,12 +1025,8 @@ github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
|
||||
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781 h1:BUdwkIlf8IS2FasrrPg8gGPHQPOrQ18MS1Oew2tmGtY=
|
||||
github.com/cs3org/go-cs3apis v0.0.0-20231023073225-7748710e0781/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240522140613-fae5c105158f h1:gQNgViSZs4DogTJNQ7yiWGPS+HbYwhccSw6OjYQYxlk=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240522140613-fae5c105158f/go.mod h1:BOlJApKFrWRiaOoBCRxCTG5bghTTMlYaEZrRxOzKaS8=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a h1:Xv2TmFIxhk642vN3NO03XSZl9NO0TVy7/QAC0CUiySc=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a/go.mod h1:BOlJApKFrWRiaOoBCRxCTG5bghTTMlYaEZrRxOzKaS8=
|
||||
github.com/cs3org/reva/v2 v2.19.7 h1:0hMIbCZq1VBSxAslEvDDkj1Si8RqxbxR0wx3JvIRXTE=
|
||||
github.com/cs3org/reva/v2 v2.19.7/go.mod h1:GRUrOp5HbFVwZTgR9bVrMZ/MvVy+Jhxw1PdMmhhKP9E=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e h1:nooOncj0aaM6S8TuNSLY4d7KEb8xSzO9/LfqL0loV60=
|
||||
github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e/go.mod h1:BOlJApKFrWRiaOoBCRxCTG5bghTTMlYaEZrRxOzKaS8=
|
||||
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
|
||||
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
|
||||
@@ -2032,6 +2028,7 @@ github.com/thanhpk/randstr v1.0.6 h1:psAOktJFD4vV9NEVb3qkhRSMvYh4ORRaj1+w/hn4B+o
|
||||
github.com/thanhpk/randstr v1.0.6/go.mod h1:M/H2P1eNLZzlDwAzpkkkUvoyNNMbzRGhESZuEQk3r0U=
|
||||
github.com/thejerf/suture/v4 v4.0.5 h1:F1E/4FZwXWqvlWDKEUo6/ndLtxGAUzMmNqkrMknZbAA=
|
||||
github.com/thejerf/suture/v4 v4.0.5/go.mod h1:gu9Y4dXNUWFrByqRt30Rm9/UZ0wzRSt9AJS6xu/ZGxU=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
@@ -2039,6 +2036,8 @@ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JT
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 h1:PM5hJF7HVfNWmCjMdEfbuOBNXSVF2cMFGgQTPdKCbwM=
|
||||
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208/go.mod h1:BzWtXXrXzZUvMacR0oF/fbDDgUPO8L36tDMmRAf14ns=
|
||||
|
||||
35
ocis-pkg/capabilities/defaults.go
Normal file
35
ocis-pkg/capabilities/defaults.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package capabilities
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
// allow the consuming part to change defaults, e.g., tests
|
||||
var defaultCapabilities atomic.Pointer[ocs.Capabilities]
|
||||
|
||||
func init() { //nolint:gochecknoinits
|
||||
ResetDefault()
|
||||
}
|
||||
|
||||
// ResetDefault resets the default [Capabilities] to the default values.
|
||||
func ResetDefault() {
|
||||
defaultCapabilities.Store(
|
||||
&ocs.Capabilities{
|
||||
Theme: &ocs.CapabilitiesTheme{
|
||||
Logo: &ocs.CapabilitiesThemeLogo{
|
||||
PermittedFileTypes: map[string]string{
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Default returns the default [Capabilities].
|
||||
func Default() *ocs.Capabilities { return defaultCapabilities.Load() }
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/capabilities"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/config/defaults"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
@@ -290,6 +291,7 @@ func FrontendConfigFromStruct(cfg *config.Config, logger log.Logger) (map[string
|
||||
"share_jail": true,
|
||||
"max_quota": cfg.MaxQuota,
|
||||
},
|
||||
"theme": capabilities.Default().Theme,
|
||||
"search": map[string]interface{}{
|
||||
"property": map[string]interface{}{
|
||||
"name": map[string]interface{}{
|
||||
|
||||
@@ -43,6 +43,28 @@ This feature is useful for organizations that want to provide third party or cus
|
||||
It's important to note that the feature at the moment is only capable of providing static (js, mjs, e.g.) web applications
|
||||
and does not support injection of dynamic web applications (custom dynamic backends).
|
||||
|
||||
### Loading Themes
|
||||
|
||||
Web themes are loaded, if added in the Infinite Scale source code, at build-time from
|
||||
`<ocis_repo>/services/web/assets/themes`.
|
||||
This cannot be manipulated at runtime.
|
||||
|
||||
Additionally, the administrator can provide custom themes by storing it in the path defined by the environment
|
||||
variable `WEB_ASSET_THEMES_PATH`.
|
||||
|
||||
With the theme root directory defined, the system needs to know which theme to use.
|
||||
This can be done by setting the `WEB_UI_THEME_PATH` environment variable.
|
||||
|
||||
The final theme is composed of the built-in and the custom theme provided by the
|
||||
administrator via `WEB_ASSET_THEMES_PATH` and `WEB_UI_THEME_PATH`.
|
||||
|
||||
For example, Infinite Scale by default contains a built-in ownCloud theme.
|
||||
If the administrator provides a custom theme via the `WEB_ASSET_THEMES_PATH` directory like,
|
||||
`WEB_ASSET_THEMES_PATH/owncloud/themes.json`, this one will be used instead of the built-in one.
|
||||
|
||||
Some theme keys are mandatory, like the `common.shareRoles` settings.
|
||||
Such mandatory keys are injected automatically at runtime if not provided.
|
||||
|
||||
### Loading Applications
|
||||
|
||||
Web applications are loaded, if added in the Infinite Scale source code, at build-time from
|
||||
@@ -57,7 +79,7 @@ but can be redefined with any path set manually.
|
||||
The final list of available applications is composed of the built-in and the custom applications provided by the
|
||||
administrator via `WEB_ASSET_APPS_PATH`.
|
||||
|
||||
For example, if Infinite Scale would contain a built-in extension named `image-viewer-dfx` and the administrator provides a custom application named `image-viewer-obj` via the `WEB_ASSET_APPS_PATH` directory, the user will be able to access both
|
||||
For example, if Infinite Scale contains a built-in extension named `image-viewer-dfx` and the administrator provides a custom application named `image-viewer-obj` via the `WEB_ASSET_APPS_PATH` directory, the user will be able to access both
|
||||
applications from the WebUI.
|
||||
|
||||
### Application Structure
|
||||
|
||||
0
services/web/assets/themes/.keep
Normal file
0
services/web/assets/themes/.keep
Normal file
198
services/web/assets/themes/owncloud/theme.json
Normal file
198
services/web/assets/themes/owncloud/theme.json
Normal file
@@ -0,0 +1,198 @@
|
||||
{
|
||||
"common": {
|
||||
"name": "ownCloud",
|
||||
"slogan": "ownCloud – A safe home for all your data",
|
||||
"logo": "themes/owncloud/assets/logo.svg",
|
||||
"urls": {
|
||||
"accessDeniedHelp": "",
|
||||
"imprint": "",
|
||||
"privacy": ""
|
||||
}
|
||||
},
|
||||
"clients": {
|
||||
"android": {},
|
||||
"desktop": {},
|
||||
"ios": {},
|
||||
"web": {
|
||||
"defaults": {
|
||||
"appBanner": {},
|
||||
"logo": {
|
||||
"topbar": "themes/owncloud/assets/logo.svg",
|
||||
"favicon": "themes/owncloud/assets/favicon.jpg",
|
||||
"login": "themes/owncloud/assets/logo.svg"
|
||||
},
|
||||
"loginPage": {
|
||||
"backgroundImg": "themes/owncloud/assets/loginBackground.jpg"
|
||||
},
|
||||
"designTokens": {
|
||||
"breakpoints": {
|
||||
"xsmall-max": "",
|
||||
"small-default": "",
|
||||
"small-max": "",
|
||||
"medium-default": "",
|
||||
"medium-max": "",
|
||||
"large-default": "",
|
||||
"large-max": "",
|
||||
"xlarge": ""
|
||||
},
|
||||
"fontSizes": {
|
||||
"default": "",
|
||||
"large": "",
|
||||
"medium": ""
|
||||
},
|
||||
"sizes": {
|
||||
"form-check-default": "",
|
||||
"height-small": "",
|
||||
"height-table-row": "",
|
||||
"icon-default": "",
|
||||
"max-height-logo": "",
|
||||
"max-width-logo": "",
|
||||
"width-medium": "",
|
||||
"tiles-default": "",
|
||||
"tiles-resize-step": ""
|
||||
},
|
||||
"spacing": {
|
||||
"xsmall": "",
|
||||
"small": "",
|
||||
"medium": "",
|
||||
"large": "",
|
||||
"xlarge": "",
|
||||
"xxlarge": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"themes": [
|
||||
{
|
||||
"isDark": false,
|
||||
"name": "Light Theme",
|
||||
"designTokens": {
|
||||
"colorPalette": {
|
||||
"background-accentuate": "rgba(255, 255, 5, 0.1)",
|
||||
"background-default": "#ffffff",
|
||||
"background-highlight": "#edf3fa",
|
||||
"background-muted": "#f8f8f8",
|
||||
"background-secondary": "#ffffff",
|
||||
"background-hover": "rgb(236, 236, 236)",
|
||||
"color-components-apptopbar-background": "transparent",
|
||||
"color-components-apptopbar-border": "#ceddee",
|
||||
"border": "#ecebee",
|
||||
"input-bg": "#ffffff",
|
||||
"input-border": "#ceddee",
|
||||
"input-text-default": "#041e42",
|
||||
"input-text-muted": "#4c5f79",
|
||||
"swatch-brand-default": "#041e42",
|
||||
"swatch-brand-hover": "#223959",
|
||||
"swatch-brand-contrast": "#ffffff",
|
||||
"swatch-danger-contrast": "#ffffff",
|
||||
"swatch-danger-default": "rgb(197, 48, 48)",
|
||||
"swatch-danger-hover": "#b12b2b",
|
||||
"swatch-danger-muted": "rgb(204, 117, 117)",
|
||||
"swatch-inverse-default": "#ffffff",
|
||||
"swatch-inverse-hover": "#ffffff",
|
||||
"swatch-inverse-muted": "#bfbfbf",
|
||||
"swatch-passive-default": "#4c5f79",
|
||||
"swatch-passive-hover": "#43536b",
|
||||
"swatch-passive-hover-outline": "#f7fafd",
|
||||
"swatch-passive-muted": "#283e5d",
|
||||
"swatch-passive-contrast": "#ffffff",
|
||||
"swatch-primary-default": "#4a76ac",
|
||||
"swatch-primary-hover": "#80a7d7",
|
||||
"swatch-primary-muted": "#2c588e",
|
||||
"swatch-primary-muted-hover": "rgb(36, 75, 119)",
|
||||
"swatch-primary-gradient": "#4e85c8",
|
||||
"swatch-primary-gradient-hover": "rgb(59, 118, 194)",
|
||||
"swatch-primary-contrast": "#ffffff",
|
||||
"swatch-success-default": "rgb(3, 84, 63)",
|
||||
"swatch-success-hover": "#023b2c",
|
||||
"swatch-success-muted": "rgb(83, 150, 10)",
|
||||
"swatch-success-contrast": "#ffffff",
|
||||
"swatch-warning-default": "rgb(183, 76, 27)",
|
||||
"swatch-warning-hover": "#a04318",
|
||||
"swatch-warning-muted": "rgba(183, 76, 27, .5)",
|
||||
"swatch-warning-contrast": "#ffffff",
|
||||
"text-default": "#041e42",
|
||||
"text-inverse": "#ffffff",
|
||||
"text-muted": "#4c5f79",
|
||||
"icon-folder": "#4d7eaf",
|
||||
"icon-archive": "#fbbe54",
|
||||
"icon-image": "#ee6b3b",
|
||||
"icon-spreadsheet": "#15c286",
|
||||
"icon-document": "#3b44a6",
|
||||
"icon-video": "#045459",
|
||||
"icon-audio": "#700460",
|
||||
"icon-presentation": "#ee6b3b",
|
||||
"icon-pdf": "#ec0d47",
|
||||
"icon-medical": "#0984db"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"isDark": true,
|
||||
"name": "Dark Theme",
|
||||
"designTokens": {
|
||||
"colorPalette": {
|
||||
"background-accentuate": "#696969",
|
||||
"background-default": "#292929",
|
||||
"background-highlight": "#383838",
|
||||
"background-muted": "#383838",
|
||||
"background-secondary": "#4f4f4f",
|
||||
"background-hover": "#383838",
|
||||
"color-components-apptopbar-background": "transparent",
|
||||
"color-components-apptopbar-border": "#ceddee",
|
||||
"border": "#383838",
|
||||
"input-bg": "#4f4f4f",
|
||||
"input-border": "#696969",
|
||||
"input-text-default": "#dadcdf",
|
||||
"input-text-muted": "#bdbfc3",
|
||||
"swatch-brand-default": "#212121",
|
||||
"swatch-brand-hover": "#ffffff",
|
||||
"swatch-brand-contrast": "#dadcdf",
|
||||
"swatch-inverse-default": "",
|
||||
"swatch-inverse-hover": "",
|
||||
"swatch-inverse-muted": "#696969",
|
||||
"swatch-passive-default": "#c2c2c2",
|
||||
"swatch-passive-hover": "",
|
||||
"swatch-passive-hover-outline": "#3B3B3B",
|
||||
"swatch-passive-muted": "#bdbfc3",
|
||||
"swatch-passive-contrast": "#000000",
|
||||
"swatch-primary-default": "#73b0f2",
|
||||
"swatch-primary-hover": "#7bafef",
|
||||
"swatch-primary-muted": "",
|
||||
"swatch-primary-muted-hover": "#2282f7",
|
||||
"swatch-primary-gradient": "#4e85c8",
|
||||
"swatch-primary-gradient-hover": "#76a1d5",
|
||||
"swatch-primary-contrast": "#dadcdf",
|
||||
"swatch-success-background": "rgba(0, 188, 140, 0)",
|
||||
"swatch-success-default": "rgb(0, 188, 140)",
|
||||
"swatch-success-hover": "#00f0b4",
|
||||
"swatch-success-muted": "rgba(0, 188, 140, .5)",
|
||||
"swatch-success-contrast": "#000000",
|
||||
"swatch-warning-background": "rgba(0,0,0,0)",
|
||||
"swatch-warning-default": "rgb(232, 191, 73)",
|
||||
"swatch-warning-hover": "#eed077",
|
||||
"swatch-warning-muted": "rgba(232, 178, 19, .5)",
|
||||
"swatch-danger-default": "rgb(255, 72, 53)",
|
||||
"swatch-danger-hover": "#ff7566",
|
||||
"swatch-danger-muted": "rgba(255, 72, 53, .5)",
|
||||
"swatch-danger-contrast": "#dadcdf",
|
||||
"swatch-warning-contrast": "#000000",
|
||||
"text-default": "#dadcdf",
|
||||
"text-inverse": "#000000",
|
||||
"text-muted": "#c2c2c2",
|
||||
"icon-folder": "rgb(44, 101, 255)",
|
||||
"icon-archive": "rgb(255, 207, 1)",
|
||||
"icon-image": "rgb(255, 111, 0)",
|
||||
"icon-spreadsheet": "rgb(0, 182, 87)",
|
||||
"icon-document": "rgb(44, 101, 255)",
|
||||
"icon-video": "rgb(0, 187, 219)",
|
||||
"icon-audio": "rgb(208, 67, 236)",
|
||||
"icon-presentation": "rgb(255, 64, 6)",
|
||||
"icon-pdf": "rgb(225, 5, 14)",
|
||||
"icon-medical": "rgb(9,132,219)"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,8 +32,9 @@ type Config struct {
|
||||
// Asset defines the available asset configuration.
|
||||
type Asset struct {
|
||||
DeprecatedPath string `yaml:"path" env:"WEB_ASSET_PATH" desc:"Serve ownCloud Web assets from a path on the filesystem instead of the builtin assets." introductionVersion:"pre5.0" deprecationVersion:"5.1.0" removalVersion:"6.0.0" deprecationInfo:"The WEB_ASSET_PATH is deprecated and will be removed in the future." deprecationReplacement:"Use WEB_ASSET_CORE_PATH instead."`
|
||||
CorePath string `yaml:"core_path" env:"WEB_ASSET_CORE_PATH" desc:"Serve ownCloud Web assets from a path on the filesystem instead of the builtin assets." introductionVersion:"5.1"`
|
||||
AppsPath string `yaml:"apps_path" env:"WEB_ASSET_APPS_PATH" desc:"Serve ownCloud Web apps assets from a path on the filesystem instead of the builtin assets." introductionVersion:"5.1"`
|
||||
CorePath string `yaml:"core_path" env:"WEB_ASSET_CORE_PATH" desc:"Serve ownCloud Web assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/web/assets/core" introductionVersion:"5.1"`
|
||||
ThemesPath string `yaml:"themes_path" env:"OCIS_ASSET_THEMES_PATH;WEB_ASSET_THEMES_PATH" desc:"Serve ownCloud themes from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/web/assets/themes" introductionVersion:"5.1"`
|
||||
AppsPath string `yaml:"apps_path" env:"WEB_ASSET_APPS_PATH" desc:"Serve ownCloud Web apps assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OCIS_BASE_DATA_PATH:/web/assets/apps" introductionVersion:"5.1"`
|
||||
}
|
||||
|
||||
// CustomStyle references additional css to be loaded into ownCloud Web.
|
||||
|
||||
@@ -80,8 +80,9 @@ func DefaultConfig() *config.Config {
|
||||
Name: "web",
|
||||
},
|
||||
Asset: config.Asset{
|
||||
CorePath: filepath.Join(defaults.BaseDataPath(), "web/assets/core"),
|
||||
AppsPath: filepath.Join(defaults.BaseDataPath(), "web/assets/apps"),
|
||||
CorePath: filepath.Join(defaults.BaseDataPath(), "web/assets/core"),
|
||||
AppsPath: filepath.Join(defaults.BaseDataPath(), "web/assets/apps"),
|
||||
ThemesPath: filepath.Join(defaults.BaseDataPath(), "web/assets/themes"),
|
||||
},
|
||||
GatewayAddress: "com.owncloud.api.gateway",
|
||||
Web: config.Web{
|
||||
|
||||
@@ -56,15 +56,10 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
coreFS := fsx.NewFallbackFS(
|
||||
fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.CorePath),
|
||||
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/core"),
|
||||
)
|
||||
appsFS := fsx.NewFallbackFS(
|
||||
fsx.NewReadOnlyFs(fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.AppsPath)),
|
||||
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/apps"),
|
||||
)
|
||||
|
||||
// build and inject the list of applications into the config
|
||||
for _, application := range apps.List(options.Logger, options.Config.Apps, appsFS.Secondary().IOFS(), appsFS.Primary().IOFS()) {
|
||||
options.Config.Web.Config.ExternalApps = append(
|
||||
@@ -73,10 +68,31 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
)
|
||||
}
|
||||
|
||||
handle := svc.NewService(
|
||||
coreFS := fsx.NewFallbackFS(
|
||||
fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.CorePath),
|
||||
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/core"),
|
||||
)
|
||||
themeFS := fsx.NewFallbackFS(
|
||||
fsx.NewBasePathFs(fsx.NewOsFs(), options.Config.Asset.ThemesPath),
|
||||
fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes"),
|
||||
)
|
||||
// oCis is Apache licensed, and the ownCloud branding is AGPLv3.
|
||||
// we are not allowed to have the ownCloud branding as part of the oCIS repository,
|
||||
// as workaround we layer the embedded core fs on top of the theme fs to provide the ownCloud branding.
|
||||
// each asset that is part of the embedded core fs (coreFS secondary fs)
|
||||
// is downloaded at build time from the ownCloud web repository,
|
||||
// web is licensed under AGPLv3 too, and is allowed to contain the ownCloud branding.
|
||||
// themeFS = themeFS.Primary (rw) < themeFS.Secondary (ro) < coreFS.Secondary (ro)
|
||||
themeFS = fsx.NewFallbackFS(
|
||||
themeFS,
|
||||
fsx.NewBasePathFs(coreFS.Secondary(), "themes"),
|
||||
)
|
||||
|
||||
handle, err := svc.NewService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.CoreFS(coreFS),
|
||||
svc.CoreFS(coreFS.IOFS()),
|
||||
svc.AppFS(appsFS.IOFS()),
|
||||
svc.ThemeFS(themeFS),
|
||||
svc.AppsHTTPEndpoint(_customAppsEndpoint),
|
||||
svc.Config(options.Config),
|
||||
svc.GatewaySelector(gatewaySelector),
|
||||
@@ -103,6 +119,10 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
svc.TraceProvider(options.TraceProvider),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return http.Service{}, err
|
||||
}
|
||||
|
||||
{
|
||||
handle = svc.NewInstrument(handle, options.Metrics)
|
||||
handle = svc.NewLogging(handle, options.Logger)
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var (
|
||||
errInvalidThemeConfig = errors.New("invalid themes config")
|
||||
_themesConfigPath = filepath.FromSlash("themes/owncloud/theme.json")
|
||||
_allowedExtensionMediatypes = map[string]string{
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
}
|
||||
)
|
||||
|
||||
// UploadLogo implements the endpoint to upload a custom logo for the oCIS instance.
|
||||
func (p Web) UploadLogo(w http.ResponseWriter, r *http.Request) {
|
||||
gatewayClient, err := p.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := revactx.ContextMustGetUser(r.Context())
|
||||
rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{
|
||||
Permission: "Logo.Write",
|
||||
SubjectRef: &permissionsapi.SubjectReference{
|
||||
Spec: &permissionsapi.SubjectReference_UserId{
|
||||
UserId: user.Id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rsp.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
file, fileHeader, err := r.FormFile("logo")
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
mediatype := fileHeader.Header.Get("Content-Type")
|
||||
if !allowedFiletype(fileHeader.Filename, mediatype) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fp := filepath.Join("branding", filepath.Join("/", fileHeader.Filename))
|
||||
err = afero.WriteReader(p.coreFS, fp, file)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = p.updateLogoThemeConfig(fp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// ResetLogo implements the endpoint to reset the instance logo.
|
||||
// The config will be changed back to use the embedded logo asset.
|
||||
func (p Web) ResetLogo(w http.ResponseWriter, r *http.Request) {
|
||||
gatewayClient, err := p.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := revactx.ContextMustGetUser(r.Context())
|
||||
rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{
|
||||
Permission: "Logo.Write",
|
||||
SubjectRef: &permissionsapi.SubjectReference{
|
||||
Spec: &permissionsapi.SubjectReference_UserId{
|
||||
UserId: user.Id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rsp.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
f, err := p.coreFS.Secondary().Open(_themesConfigPath)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
originalPath, err := p.getLogoPath(f)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if err := p.updateLogoThemeConfig(originalPath); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p Web) getLogoPath(r io.Reader) (string, error) {
|
||||
// This decoding of the themes.json file is not optimal. If we need to decode it for other
|
||||
// usecases as well we should consider decoding to a struct.
|
||||
var m map[string]interface{}
|
||||
_ = json.NewDecoder(r).Decode(&m)
|
||||
|
||||
logoCfg, ok := extractMap(m, "clients", "web", "defaults", "logo")
|
||||
if !ok {
|
||||
return "", errInvalidThemeConfig
|
||||
}
|
||||
|
||||
logoPath, ok := logoCfg["login"].(string)
|
||||
if !ok {
|
||||
return "", errInvalidThemeConfig
|
||||
}
|
||||
|
||||
return logoPath, nil
|
||||
}
|
||||
|
||||
func (p Web) updateLogoThemeConfig(logoPath string) error {
|
||||
f, err := p.coreFS.Open(_themesConfigPath)
|
||||
if err == nil {
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
// This decoding of the themes.json file is not optimal. If we need to decode it for other
|
||||
// usecases as well we should consider decoding to a struct.
|
||||
var m map[string]interface{}
|
||||
_ = json.NewDecoder(f).Decode(&m)
|
||||
|
||||
// change logo in common part
|
||||
commonCfg, ok := extractMap(m, "common")
|
||||
if !ok {
|
||||
return errInvalidThemeConfig
|
||||
}
|
||||
commonCfg["logo"] = logoPath
|
||||
|
||||
logoCfg, ok := extractMap(m, "clients", "web", "defaults", "logo")
|
||||
if !ok {
|
||||
return errInvalidThemeConfig
|
||||
}
|
||||
|
||||
logoCfg["login"] = logoPath
|
||||
logoCfg["topbar"] = logoPath
|
||||
|
||||
dst, err := p.coreFS.Create(_themesConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
return json.NewEncoder(dst).Encode(m)
|
||||
}
|
||||
|
||||
func allowedFiletype(filename, mediatype string) bool {
|
||||
ext := path.Ext(filename)
|
||||
|
||||
// Check if we allow that extension and if the mediatype matches the extension
|
||||
mt, ok := _allowedExtensionMediatypes[ext]
|
||||
return ok && mt == mediatype
|
||||
}
|
||||
|
||||
// extractMap extracts embedded map[string]interface{} by the keys chain
|
||||
func extractMap(data map[string]interface{}, keys ...string) (map[string]interface{}, bool) {
|
||||
last := data
|
||||
var ok bool
|
||||
for _, key := range keys {
|
||||
last, ok = last[key].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
return last, true
|
||||
}
|
||||
@@ -28,13 +28,3 @@ func (i instrument) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (i instrument) Config(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.Config(w, r)
|
||||
}
|
||||
|
||||
// UploadLogo implements the Service interface.
|
||||
func (i instrument) UploadLogo(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.UploadLogo(w, r)
|
||||
}
|
||||
|
||||
// ResetLogo implements the Service interface.
|
||||
func (i instrument) ResetLogo(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ResetLogo(w, r)
|
||||
}
|
||||
|
||||
@@ -28,13 +28,3 @@ func (l logging) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (l logging) Config(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.Config(w, r)
|
||||
}
|
||||
|
||||
// UploadLogo implements the Service interface.
|
||||
func (l logging) UploadLogo(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.UploadLogo(w, r)
|
||||
}
|
||||
|
||||
// ResetLogo implements the Service interface.
|
||||
func (l logging) ResetLogo(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ResetLogo(w, r)
|
||||
}
|
||||
|
||||
@@ -23,9 +23,10 @@ type Options struct {
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
GatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
TraceProvider trace.TracerProvider
|
||||
AppFS fs.FS
|
||||
AppsHTTPEndpoint string
|
||||
CoreFS *fsx.FallbackFS
|
||||
CoreFS fs.FS
|
||||
AppFS fs.FS
|
||||
ThemeFS *fsx.FallbackFS
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -81,6 +82,13 @@ func AppFS(val fs.FS) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// ThemeFS provides a function to set the themeFS option.
|
||||
func ThemeFS(val *fsx.FallbackFS) Option {
|
||||
return func(o *Options) {
|
||||
o.ThemeFS = val
|
||||
}
|
||||
}
|
||||
|
||||
// AppsHTTPEndpoint provides a function to set the appsHTTPEndpoint option.
|
||||
func AppsHTTPEndpoint(val string) Option {
|
||||
return func(o *Options) {
|
||||
@@ -89,7 +97,7 @@ func AppsHTTPEndpoint(val string) Option {
|
||||
}
|
||||
|
||||
// CoreFS provides a function to set the coreFS option.
|
||||
func CoreFS(val *fsx.FallbackFS) Option {
|
||||
func CoreFS(val fs.FS) Option {
|
||||
return func(o *Options) {
|
||||
o.CoreFS = val
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/assets"
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/theme"
|
||||
)
|
||||
|
||||
// ErrConfigInvalid is returned when the config parse is invalid.
|
||||
@@ -30,14 +31,12 @@ var ErrConfigInvalid = `Invalid or missing config`
|
||||
|
||||
// Service defines the service handlers.
|
||||
type Service interface {
|
||||
ServeHTTP(http.ResponseWriter, *http.Request)
|
||||
Config(http.ResponseWriter, *http.Request)
|
||||
UploadLogo(http.ResponseWriter, *http.Request)
|
||||
ResetLogo(http.ResponseWriter, *http.Request)
|
||||
ServeHTTP(w http.ResponseWriter, r *http.Request)
|
||||
Config(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
// NewService returns a service implementation for Service.
|
||||
func NewService(opts ...Option) Service {
|
||||
func NewService(opts ...Option) (Service, error) {
|
||||
options := newOptions(opts...)
|
||||
|
||||
m := chi.NewMux()
|
||||
@@ -57,9 +56,19 @@ func NewService(opts ...Option) Service {
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
coreFS: options.CoreFS,
|
||||
themeFS: options.ThemeFS,
|
||||
gatewaySelector: options.GatewaySelector,
|
||||
}
|
||||
|
||||
themeService, err := theme.NewService(
|
||||
theme.ServiceOptions{}.
|
||||
WithThemeFS(options.ThemeFS).
|
||||
WithGatewaySelector(options.GatewaySelector),
|
||||
)
|
||||
if err != nil {
|
||||
return svc, err
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
r.Get("/config.json", svc.Config)
|
||||
r.Route("/branding/logo", func(r chi.Router) {
|
||||
@@ -67,8 +76,16 @@ func NewService(opts ...Option) Service {
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret),
|
||||
))
|
||||
r.Post("/", svc.UploadLogo)
|
||||
r.Delete("/", svc.ResetLogo)
|
||||
r.Post("/", themeService.LogoUpload)
|
||||
r.Delete("/", themeService.LogoReset)
|
||||
})
|
||||
r.Route("/themes", func(r chi.Router) {
|
||||
r.Get("/{id}/theme.json", themeService.Get)
|
||||
r.Mount("/", svc.Static(
|
||||
options.ThemeFS.IOFS(),
|
||||
path.Join(svc.config.HTTP.Root, "/themes"),
|
||||
options.Config.HTTP.CacheTTL,
|
||||
))
|
||||
})
|
||||
r.Mount(options.AppsHTTPEndpoint, svc.Static(
|
||||
options.AppFS,
|
||||
@@ -76,18 +93,17 @@ func NewService(opts ...Option) Service {
|
||||
options.Config.HTTP.CacheTTL,
|
||||
))
|
||||
r.Mount("/", svc.Static(
|
||||
svc.coreFS.IOFS(),
|
||||
svc.coreFS,
|
||||
svc.config.HTTP.Root,
|
||||
options.Config.HTTP.CacheTTL,
|
||||
))
|
||||
})
|
||||
|
||||
_ = chi.Walk(m, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
||||
options.Logger.Debug().Str("method", method).Str("route", route).Int("middlewares", len(middlewares)).Msg("serving endpoint")
|
||||
return nil
|
||||
})
|
||||
|
||||
return svc
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
// Web defines the handlers for the web service.
|
||||
@@ -95,7 +111,8 @@ type Web struct {
|
||||
logger log.Logger
|
||||
config *config.Config
|
||||
mux *chi.Mux
|
||||
coreFS *fsx.FallbackFS
|
||||
coreFS fs.FS
|
||||
themeFS *fsx.FallbackFS
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
}
|
||||
|
||||
|
||||
3
services/web/pkg/theme/export_test.go
Normal file
3
services/web/pkg/theme/export_test.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package theme
|
||||
|
||||
var IsFiletypePermitted = isFiletypePermitted
|
||||
98
services/web/pkg/theme/kv.go
Normal file
98
services/web/pkg/theme/kv.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
|
||||
"dario.cat/mergo"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/tidwall/sjson"
|
||||
)
|
||||
|
||||
// KV is a generic key-value map.
|
||||
type KV map[string]any
|
||||
|
||||
// MergeKV merges the given key-value maps.
|
||||
func MergeKV(values ...KV) (KV, error) {
|
||||
var kv KV
|
||||
|
||||
for _, v := range values {
|
||||
err := mergo.Merge(&kv, v, mergo.WithOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// PatchKV injects the given values into to v.
|
||||
func PatchKV(v any, values KV) error {
|
||||
bv, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nv := string(bv)
|
||||
|
||||
for k, val := range values {
|
||||
var err error
|
||||
switch val {
|
||||
// if the value is nil, we delete the key
|
||||
case nil:
|
||||
nv, err = sjson.Delete(nv, k)
|
||||
default:
|
||||
nv, err = sjson.Set(nv, k, val)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return json.Unmarshal([]byte(nv), v)
|
||||
}
|
||||
|
||||
// LoadKV loads a key-value map from the given file system.
|
||||
func LoadKV(fsys afero.Fs, p string) (KV, error) {
|
||||
f, err := fsys.Open(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var kv KV
|
||||
err = json.NewDecoder(f).Decode(&kv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return kv, nil
|
||||
}
|
||||
|
||||
// WriteKV writes the given key-value map to the file system.
|
||||
func WriteKV(fsys afero.Fs, p string, kv KV) error {
|
||||
data, err := json.Marshal(kv)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return afero.WriteReader(fsys, p, bytes.NewReader(data))
|
||||
}
|
||||
|
||||
// UpdateKV updates the key-value map at the given path with the given values.
|
||||
func UpdateKV(fsys afero.Fs, p string, values KV) error {
|
||||
var kv KV
|
||||
|
||||
existing, err := LoadKV(fsys, p)
|
||||
if err == nil {
|
||||
kv = existing
|
||||
}
|
||||
|
||||
err = PatchKV(&kv, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return WriteKV(fsys, p, kv)
|
||||
}
|
||||
137
services/web/pkg/theme/kv_test.go
Normal file
137
services/web/pkg/theme/kv_test.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package theme_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/theme"
|
||||
)
|
||||
|
||||
func TestMergeKV(t *testing.T) {
|
||||
left := theme.KV{
|
||||
"left": "left",
|
||||
"both": "left",
|
||||
}
|
||||
right := theme.KV{
|
||||
"right": "right",
|
||||
"both": "right",
|
||||
}
|
||||
|
||||
result, err := theme.MergeKV(left, right)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, result, theme.KV{
|
||||
"left": "left",
|
||||
"right": "right",
|
||||
"both": "right",
|
||||
})
|
||||
}
|
||||
|
||||
func TestPatchKV(t *testing.T) {
|
||||
in := theme.KV{
|
||||
"a": theme.KV{
|
||||
"value": "a",
|
||||
},
|
||||
"b": theme.KV{
|
||||
"value": "b",
|
||||
},
|
||||
}
|
||||
err := theme.PatchKV(&in, theme.KV{
|
||||
"b.value": "b-new",
|
||||
"c.value": "c-new",
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, in, theme.KV{
|
||||
"a": map[string]interface{}{
|
||||
"value": "a",
|
||||
},
|
||||
"b": map[string]interface{}{
|
||||
"value": "b-new",
|
||||
},
|
||||
"c": map[string]interface{}{
|
||||
"value": "c-new",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoadKV(t *testing.T) {
|
||||
in := theme.KV{
|
||||
"a": map[string]interface{}{
|
||||
"value": "a",
|
||||
},
|
||||
"b": map[string]interface{}{
|
||||
"value": "b",
|
||||
},
|
||||
}
|
||||
b, err := json.Marshal(in)
|
||||
assert.Nil(t, err)
|
||||
|
||||
fsys := fsx.NewMemMapFs()
|
||||
assert.Nil(t, afero.WriteFile(fsys, "some.json", b, 0644))
|
||||
|
||||
out, err := theme.LoadKV(fsys, "some.json")
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, in, out)
|
||||
}
|
||||
|
||||
func TestWriteKV(t *testing.T) {
|
||||
in := theme.KV{
|
||||
"a": map[string]interface{}{
|
||||
"value": "a",
|
||||
},
|
||||
"b": map[string]interface{}{
|
||||
"value": "b",
|
||||
},
|
||||
}
|
||||
|
||||
fsys := fsx.NewMemMapFs()
|
||||
assert.Nil(t, theme.WriteKV(fsys, "some.json", in))
|
||||
|
||||
f, err := fsys.Open("some.json")
|
||||
assert.Nil(t, err)
|
||||
|
||||
var out theme.KV
|
||||
assert.Nil(t, json.NewDecoder(f).Decode(&out))
|
||||
assert.Equal(t, in, out)
|
||||
}
|
||||
|
||||
func TestUpdateKV(t *testing.T) {
|
||||
fileKV := theme.KV{
|
||||
"a": map[string]interface{}{
|
||||
"value": "a",
|
||||
},
|
||||
"b": map[string]interface{}{
|
||||
"value": "b",
|
||||
},
|
||||
}
|
||||
|
||||
wb, err := json.Marshal(fileKV)
|
||||
assert.Nil(t, err)
|
||||
|
||||
fsys := fsx.NewMemMapFs()
|
||||
assert.Nil(t, afero.WriteFile(fsys, "some.json", wb, 0644))
|
||||
assert.Nil(t, theme.UpdateKV(fsys, "some.json", theme.KV{
|
||||
"b.value": "b-new",
|
||||
"c.value": "c-new",
|
||||
}))
|
||||
|
||||
f, err := fsys.Open("some.json")
|
||||
assert.Nil(t, err)
|
||||
|
||||
var out theme.KV
|
||||
assert.Nil(t, json.NewDecoder(f).Decode(&out))
|
||||
assert.Equal(t, out, theme.KV{
|
||||
"a": map[string]interface{}{
|
||||
"value": "a",
|
||||
},
|
||||
"b": map[string]interface{}{
|
||||
"value": "b-new",
|
||||
},
|
||||
"c": map[string]interface{}{
|
||||
"value": "c-new",
|
||||
},
|
||||
})
|
||||
}
|
||||
197
services/web/pkg/theme/service.go
Normal file
197
services/web/pkg/theme/service.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1"
|
||||
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/path/filepathx"
|
||||
)
|
||||
|
||||
// ServiceOptions defines the options to configure the Service.
|
||||
type ServiceOptions struct {
|
||||
themeFS *fsx.FallbackFS
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
}
|
||||
|
||||
// WithThemeFS sets the theme filesystem.
|
||||
func (o ServiceOptions) WithThemeFS(fSys *fsx.FallbackFS) ServiceOptions {
|
||||
o.themeFS = fSys
|
||||
return o
|
||||
}
|
||||
|
||||
// WithGatewaySelector sets the gateway selector.
|
||||
func (o ServiceOptions) WithGatewaySelector(gws pool.Selectable[gateway.GatewayAPIClient]) ServiceOptions {
|
||||
o.gatewaySelector = gws
|
||||
return o
|
||||
}
|
||||
|
||||
// validate validates the input parameters.
|
||||
func (o ServiceOptions) validate() error {
|
||||
if o.themeFS == nil {
|
||||
return errors.New("themeFS is required")
|
||||
}
|
||||
|
||||
if o.gatewaySelector == nil {
|
||||
return errors.New("gatewaySelector is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Service defines the http service.
|
||||
type Service struct {
|
||||
themeFS *fsx.FallbackFS
|
||||
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
|
||||
}
|
||||
|
||||
// NewService initializes a new Service.
|
||||
func NewService(options ServiceOptions) (Service, error) {
|
||||
if err := options.validate(); err != nil {
|
||||
return Service{}, err
|
||||
}
|
||||
|
||||
return Service(options), nil
|
||||
}
|
||||
|
||||
// Get renders the theme, the theme is a merge of the default theme, the base theme, and the branding theme.
|
||||
func (s Service) Get(w http.ResponseWriter, r *http.Request) {
|
||||
// there is no guarantee that the theme exists, its optional; therefore, we ignore the error
|
||||
baseTheme, _ := LoadKV(s.themeFS, filepathx.JailJoin(r.PathValue("id"), _themeFileName))
|
||||
|
||||
// there is no guarantee that the theme exists, its optional; therefore, we ignore the error here too
|
||||
brandingTheme, _ := LoadKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName))
|
||||
|
||||
// merge the themes, the order is important, the last one wins and overrides the previous ones
|
||||
// themeDefaults: contains all the default values, this is guaranteed to exist
|
||||
// baseTheme: contains the base theme from the theme fs, there is no guarantee that it exists
|
||||
// brandingTheme: contains the branding theme from the theme fs, there is no guarantee that it exists
|
||||
// mergedTheme = themeDefaults < baseTheme < brandingTheme
|
||||
mergedTheme, err := MergeKV(themeDefaults, baseTheme, brandingTheme)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := json.Marshal(mergedTheme)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write(b)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// LogoUpload implements the endpoint to upload a custom logo for the oCIS instance.
|
||||
func (s Service) LogoUpload(w http.ResponseWriter, r *http.Request) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := revactx.ContextMustGetUser(r.Context())
|
||||
rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{
|
||||
Permission: "Logo.Write",
|
||||
SubjectRef: &permissionsapi.SubjectReference{
|
||||
Spec: &permissionsapi.SubjectReference_UserId{
|
||||
UserId: user.Id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rsp.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
file, fileHeader, err := r.FormFile("logo")
|
||||
if err != nil {
|
||||
if errors.Is(err, http.ErrMissingFile) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if !isFiletypePermitted(fileHeader.Filename, fileHeader.Header.Get("Content-Type")) {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
fp := filepathx.JailJoin(_brandingRoot, fileHeader.Filename)
|
||||
err = afero.WriteReader(s.themeFS, fp, file)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = UpdateKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName), KV{
|
||||
"common.logo": filepathx.JailJoin("themes", fp),
|
||||
"clients.web.defaults.logo.topbar": filepathx.JailJoin("themes", fp),
|
||||
"clients.web.defaults.logo.login": filepathx.JailJoin("themes", fp),
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
// LogoReset implements the endpoint to reset the instance logo.
|
||||
// The config will be changed back to use the embedded logo asset.
|
||||
func (s Service) LogoReset(w http.ResponseWriter, r *http.Request) {
|
||||
gatewayClient, err := s.gatewaySelector.Next()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
user := revactx.ContextMustGetUser(r.Context())
|
||||
rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{
|
||||
Permission: "Logo.Write",
|
||||
SubjectRef: &permissionsapi.SubjectReference{
|
||||
Spec: &permissionsapi.SubjectReference_UserId{
|
||||
UserId: user.Id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if rsp.Status.Code != rpc.Code_CODE_OK {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
err = UpdateKV(s.themeFS, filepathx.JailJoin(_brandingRoot, _themeFileName), KV{
|
||||
"common.logo": nil,
|
||||
"clients.web.defaults.logo.topbar": nil,
|
||||
"clients.web.defaults.logo.login": nil,
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
74
services/web/pkg/theme/service_test.go
Normal file
74
services/web/pkg/theme/service_test.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package theme_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/x/io/fsx"
|
||||
"github.com/owncloud/ocis/v2/services/graph/mocks"
|
||||
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/theme"
|
||||
)
|
||||
|
||||
func TestNewService(t *testing.T) {
|
||||
t.Run("fails if the options are invalid", func(t *testing.T) {
|
||||
_, err := theme.NewService(theme.ServiceOptions{})
|
||||
assert.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("success if the options are valid", func(t *testing.T) {
|
||||
_, err := theme.NewService(
|
||||
theme.ServiceOptions{}.
|
||||
WithThemeFS(fsx.NewFallbackFS(fsx.NewMemMapFs(), fsx.NewMemMapFs())).
|
||||
WithGatewaySelector(mocks.NewSelectable[gateway.GatewayAPIClient](t)),
|
||||
)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestService_Get(t *testing.T) {
|
||||
primaryFS := fsx.NewMemMapFs()
|
||||
fallbackFS := fsx.NewFallbackFS(primaryFS, fsx.NewMemMapFs())
|
||||
|
||||
add := func(filename string, content interface{}) {
|
||||
b, err := json.Marshal(content)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Nil(t, afero.WriteFile(primaryFS, filename, b, 0644))
|
||||
}
|
||||
|
||||
// baseTheme
|
||||
add("base/theme.json", map[string]interface{}{
|
||||
"base": "base",
|
||||
})
|
||||
// brandingTheme
|
||||
add("_branding/theme.json", map[string]interface{}{
|
||||
"_branding": "_branding",
|
||||
})
|
||||
|
||||
service, _ := theme.NewService(
|
||||
theme.ServiceOptions{}.
|
||||
WithThemeFS(fallbackFS).
|
||||
WithGatewaySelector(mocks.NewSelectable[gateway.GatewayAPIClient](t)),
|
||||
)
|
||||
r := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
r.SetPathValue("id", "base")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
service.Get(w, r)
|
||||
|
||||
jsonData := gjson.Parse(w.Body.String())
|
||||
// baseTheme
|
||||
assert.Equal(t, jsonData.Get("base").String(), "base")
|
||||
// brandingTheme
|
||||
assert.Equal(t, jsonData.Get("_branding").String(), "_branding")
|
||||
// themeDefaults
|
||||
assert.Equal(t, jsonData.Get("common.shareRoles."+unifiedrole.UnifiedRoleViewerID+".name").String(), "UnifiedRoleViewer")
|
||||
}
|
||||
61
services/web/pkg/theme/theme.go
Normal file
61
services/web/pkg/theme/theme.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package theme
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/capabilities"
|
||||
"github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole"
|
||||
)
|
||||
|
||||
var (
|
||||
_brandingRoot = "_branding"
|
||||
_themeFileName = "theme.json"
|
||||
)
|
||||
|
||||
// themeDefaults contains the default values for the theme.
|
||||
// all rendered themes get the default values from here.
|
||||
var themeDefaults = KV{
|
||||
"common": KV{
|
||||
"shareRoles": KV{
|
||||
unifiedrole.UnifiedRoleViewerID: KV{
|
||||
"name": "UnifiedRoleViewer",
|
||||
"iconName": "eye",
|
||||
},
|
||||
unifiedrole.UnifiedRoleSpaceViewerID: KV{
|
||||
"label": "UnifiedRoleSpaceViewer",
|
||||
"iconName": "eye",
|
||||
},
|
||||
unifiedrole.UnifiedRoleFileEditorID: KV{
|
||||
"label": "UnifiedRoleFileEditor",
|
||||
"iconName": "pencil",
|
||||
},
|
||||
unifiedrole.UnifiedRoleEditorID: KV{
|
||||
"label": "UnifiedRoleEditor",
|
||||
"iconName": "pencil",
|
||||
},
|
||||
unifiedrole.UnifiedRoleSpaceEditorID: KV{
|
||||
"label": "UnifiedRoleSpaceEditor",
|
||||
"iconName": "pencil",
|
||||
},
|
||||
unifiedrole.UnifiedRoleManagerID: KV{
|
||||
"label": "UnifiedRoleManager",
|
||||
"iconName": "user-star",
|
||||
},
|
||||
unifiedrole.UnifiedRoleEditorLiteID: KV{
|
||||
"label": "UnifiedRoleEditorLite",
|
||||
"iconName": "upload",
|
||||
},
|
||||
unifiedrole.UnifiedRoleSecureViewerID: KV{
|
||||
"label": "UnifiedRoleSecureView",
|
||||
"iconName": "shield",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// isFiletypePermitted checks if the given file extension is allowed.
|
||||
func isFiletypePermitted(filename string, givenMime string) bool {
|
||||
// Check if we allow that extension and if the mediatype matches the extension
|
||||
extensionMime, ok := capabilities.Default().Theme.Logo.PermittedFileTypes[path.Ext(filename)]
|
||||
return ok && extensionMime == givenMime
|
||||
}
|
||||
30
services/web/pkg/theme/theme_test.go
Normal file
30
services/web/pkg/theme/theme_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package theme_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/owncloud/ocis/v2/services/web/pkg/theme"
|
||||
)
|
||||
|
||||
// TestAllowedLogoFileTypes is here to ensure that a certain set of bare minimum file types are allowed for logos.
|
||||
func TestAllowedLogoFileTypes(t *testing.T) {
|
||||
type test struct {
|
||||
filename string
|
||||
mimetype string
|
||||
allowed bool
|
||||
}
|
||||
|
||||
tests := []test{
|
||||
{filename: "foo.jpg", mimetype: "image/jpeg", allowed: true},
|
||||
{filename: "foo.jpeg", mimetype: "image/jpeg", allowed: true},
|
||||
{filename: "foo.png", mimetype: "image/png", allowed: true},
|
||||
{filename: "foo.gif", mimetype: "image/gif", allowed: true},
|
||||
{filename: "foo.tiff", mimetype: "image/tiff", allowed: false},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
assert.Equal(t, theme.IsFiletypePermitted(tc.filename, tc.mimetype), tc.allowed)
|
||||
}
|
||||
}
|
||||
4
vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/status.go
generated
vendored
4
vendor/github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav/status.go
generated
vendored
@@ -22,13 +22,13 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data"
|
||||
"github.com/cs3org/reva/v2/pkg/appctx"
|
||||
"github.com/cs3org/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
func (s *svc) doStatus(w http.ResponseWriter, r *http.Request) {
|
||||
log := appctx.GetLogger(r.Context())
|
||||
status := &data.Status{
|
||||
status := &ocs.Status{
|
||||
Installed: true,
|
||||
Maintenance: false,
|
||||
NeedsDBUpgrade: false,
|
||||
|
||||
@@ -20,6 +20,7 @@ package config
|
||||
|
||||
import (
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data"
|
||||
"github.com/cs3org/reva/v2/pkg/owncloud/ocs"
|
||||
"github.com/cs3org/reva/v2/pkg/sharedconf"
|
||||
"github.com/cs3org/reva/v2/pkg/storage/cache"
|
||||
)
|
||||
@@ -28,7 +29,7 @@ import (
|
||||
type Config struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
Config data.ConfigData `mapstructure:"config"`
|
||||
Capabilities data.CapabilitiesData `mapstructure:"capabilities"`
|
||||
Capabilities ocs.CapabilitiesData `mapstructure:"capabilities"`
|
||||
GatewaySvc string `mapstructure:"gatewaysvc"`
|
||||
StorageregistrySvc string `mapstructure:"storage_registry_svc"`
|
||||
DefaultUploadProtocol string `mapstructure:"default_upload_protocol"`
|
||||
|
||||
@@ -22,13 +22,13 @@ import (
|
||||
"net/http"
|
||||
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config"
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data"
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response"
|
||||
"github.com/cs3org/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
// Handler renders the capability endpoint
|
||||
type Handler struct {
|
||||
c data.CapabilitiesData
|
||||
c ocs.CapabilitiesData
|
||||
defaultUploadProtocol string
|
||||
userAgentChunkingMap map[string]string
|
||||
}
|
||||
@@ -41,13 +41,13 @@ func (h *Handler) Init(c *config.Config) {
|
||||
|
||||
// capabilities
|
||||
if h.c.Capabilities == nil {
|
||||
h.c.Capabilities = &data.Capabilities{}
|
||||
h.c.Capabilities = &ocs.Capabilities{}
|
||||
}
|
||||
|
||||
// core
|
||||
|
||||
if h.c.Capabilities.Core == nil {
|
||||
h.c.Capabilities.Core = &data.CapabilitiesCore{}
|
||||
h.c.Capabilities.Core = &ocs.CapabilitiesCore{}
|
||||
}
|
||||
if h.c.Capabilities.Core.PollInterval == 0 {
|
||||
h.c.Capabilities.Core.PollInterval = 60
|
||||
@@ -58,7 +58,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// h.c.Capabilities.Core.SupportURLSigning is boolean
|
||||
|
||||
if h.c.Capabilities.Core.Status == nil {
|
||||
h.c.Capabilities.Core.Status = &data.Status{}
|
||||
h.c.Capabilities.Core.Status = &ocs.Status{}
|
||||
}
|
||||
// h.c.Capabilities.Core.Status.Installed is boolean
|
||||
// h.c.Capabilities.Core.Status.Maintenance is boolean
|
||||
@@ -85,7 +85,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// checksums
|
||||
|
||||
if h.c.Capabilities.Checksums == nil {
|
||||
h.c.Capabilities.Checksums = &data.CapabilitiesChecksums{}
|
||||
h.c.Capabilities.Checksums = &ocs.CapabilitiesChecksums{}
|
||||
}
|
||||
if h.c.Capabilities.Checksums.SupportedTypes == nil {
|
||||
h.c.Capabilities.Checksums.SupportedTypes = []string{"SHA256"}
|
||||
@@ -97,7 +97,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// files
|
||||
|
||||
if h.c.Capabilities.Files == nil {
|
||||
h.c.Capabilities.Files = &data.CapabilitiesFiles{}
|
||||
h.c.Capabilities.Files = &ocs.CapabilitiesFiles{}
|
||||
}
|
||||
|
||||
if h.c.Capabilities.Files.BlacklistedFiles == nil {
|
||||
@@ -108,17 +108,17 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// h.c.Capabilities.Files.Favorites is boolean
|
||||
|
||||
if h.c.Capabilities.Files.Archivers == nil {
|
||||
h.c.Capabilities.Files.Archivers = []*data.CapabilitiesArchiver{}
|
||||
h.c.Capabilities.Files.Archivers = []*ocs.CapabilitiesArchiver{}
|
||||
}
|
||||
|
||||
if h.c.Capabilities.Files.AppProviders == nil {
|
||||
h.c.Capabilities.Files.AppProviders = []*data.CapabilitiesAppProvider{}
|
||||
h.c.Capabilities.Files.AppProviders = []*ocs.CapabilitiesAppProvider{}
|
||||
}
|
||||
|
||||
// dav
|
||||
|
||||
if h.c.Capabilities.Dav == nil {
|
||||
h.c.Capabilities.Dav = &data.CapabilitiesDav{}
|
||||
h.c.Capabilities.Dav = &ocs.CapabilitiesDav{}
|
||||
}
|
||||
if h.c.Capabilities.Dav.Trashbin == "" {
|
||||
h.c.Capabilities.Dav.Trashbin = "1.0"
|
||||
@@ -130,24 +130,24 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// sharing
|
||||
|
||||
if h.c.Capabilities.FilesSharing == nil {
|
||||
h.c.Capabilities.FilesSharing = &data.CapabilitiesFilesSharing{}
|
||||
h.c.Capabilities.FilesSharing = &ocs.CapabilitiesFilesSharing{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.APIEnabled is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public == nil {
|
||||
h.c.Capabilities.FilesSharing.Public = &data.CapabilitiesFilesSharingPublic{}
|
||||
h.c.Capabilities.FilesSharing.Public = &ocs.CapabilitiesFilesSharingPublic{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Enabled is boolean
|
||||
h.c.Capabilities.FilesSharing.Public.Enabled = true
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public.Password == nil {
|
||||
h.c.Capabilities.FilesSharing.Public.Password = &data.CapabilitiesFilesSharingPublicPassword{}
|
||||
h.c.Capabilities.FilesSharing.Public.Password = &ocs.CapabilitiesFilesSharingPublicPassword{}
|
||||
}
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor == nil {
|
||||
h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &data.CapabilitiesFilesSharingPublicPasswordEnforcedFor{}
|
||||
h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &ocs.CapabilitiesFilesSharingPublicPasswordEnforcedFor{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadOnly is boolean
|
||||
@@ -158,7 +158,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.Password.Enforced is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.Public.ExpireDate == nil {
|
||||
h.c.Capabilities.FilesSharing.Public.ExpireDate = &data.CapabilitiesFilesSharingPublicExpireDate{}
|
||||
h.c.Capabilities.FilesSharing.Public.ExpireDate = &ocs.CapabilitiesFilesSharingPublicExpireDate{}
|
||||
}
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.ExpireDate.Enabled is boolean
|
||||
|
||||
@@ -169,7 +169,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// h.c.Capabilities.FilesSharing.IsPublic.SupportsUploadOnly is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.User == nil {
|
||||
h.c.Capabilities.FilesSharing.User = &data.CapabilitiesFilesSharingUser{}
|
||||
h.c.Capabilities.FilesSharing.User = &ocs.CapabilitiesFilesSharingUser{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.User.SendMail is boolean
|
||||
@@ -182,7 +182,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// h.c.Capabilities.FilesSharing.ShareWithMembershipGroupsOnly is boolean
|
||||
|
||||
if h.c.Capabilities.FilesSharing.UserEnumeration == nil {
|
||||
h.c.Capabilities.FilesSharing.UserEnumeration = &data.CapabilitiesFilesSharingUserEnumeration{}
|
||||
h.c.Capabilities.FilesSharing.UserEnumeration = &ocs.CapabilitiesFilesSharingUserEnumeration{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.UserEnumeration.Enabled is boolean
|
||||
@@ -192,7 +192,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
h.c.Capabilities.FilesSharing.DefaultPermissions = 31
|
||||
}
|
||||
if h.c.Capabilities.FilesSharing.Federation == nil {
|
||||
h.c.Capabilities.FilesSharing.Federation = &data.CapabilitiesFilesSharingFederation{}
|
||||
h.c.Capabilities.FilesSharing.Federation = &ocs.CapabilitiesFilesSharingFederation{}
|
||||
}
|
||||
|
||||
// h.c.Capabilities.FilesSharing.Federation.Outgoing is boolean
|
||||
@@ -205,7 +205,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// notifications
|
||||
|
||||
// if h.c.Capabilities.Notifications == nil {
|
||||
// h.c.Capabilities.Notifications = &data.CapabilitiesNotifications{}
|
||||
// h.c.Capabilities.Notifications = &ocs.CapabilitiesNotifications{}
|
||||
// }
|
||||
// if h.c.Capabilities.Notifications.Endpoints == nil {
|
||||
// h.c.Capabilities.Notifications.Endpoints = []string{"list", "get", "delete"}
|
||||
@@ -214,7 +214,7 @@ func (h *Handler) Init(c *config.Config) {
|
||||
// version
|
||||
|
||||
if h.c.Version == nil {
|
||||
h.c.Version = &data.Version{
|
||||
h.c.Version = &ocs.Version{
|
||||
// TODO get from build env
|
||||
Major: 10,
|
||||
Minor: 0,
|
||||
|
||||
@@ -21,7 +21,7 @@ package capabilities
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/data"
|
||||
"github.com/cs3org/reva/v2/pkg/owncloud/ocs"
|
||||
)
|
||||
|
||||
type chunkProtocol string
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
chunkTUS chunkProtocol = "tus"
|
||||
)
|
||||
|
||||
func (h *Handler) getCapabilitiesForUserAgent(userAgent string) data.CapabilitiesData {
|
||||
func (h *Handler) getCapabilitiesForUserAgent(userAgent string) ocs.CapabilitiesData {
|
||||
if userAgent != "" {
|
||||
for k, v := range h.userAgentChunkingMap {
|
||||
// we could also use a regexp for pattern matching
|
||||
@@ -47,7 +47,7 @@ func (h *Handler) getCapabilitiesForUserAgent(userAgent string) data.Capabilitie
|
||||
return h.c
|
||||
}
|
||||
|
||||
func setCapabilitiesForChunkProtocol(cp chunkProtocol, c *data.CapabilitiesData) {
|
||||
func setCapabilitiesForChunkProtocol(cp chunkProtocol, c *ocs.CapabilitiesData) {
|
||||
switch cp {
|
||||
case chunkV1:
|
||||
// 2.7+ will use Chunking V1 if "capabilities > files > bigfilechunking" is "true" AND "capabilities > dav > chunking" is not there
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package data
|
||||
package ocs
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
@@ -42,7 +42,7 @@ func (c ocsBool) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
return e.EncodeElement("0", start)
|
||||
}
|
||||
|
||||
// CapabilitiesData TODO document
|
||||
// CapabilitiesData holds the capabilities data
|
||||
type CapabilitiesData struct {
|
||||
Capabilities *Capabilities `json:"capabilities" xml:"capabilities"`
|
||||
Version *Version `json:"version" xml:"version"`
|
||||
@@ -59,6 +59,7 @@ type Capabilities struct {
|
||||
Graph *CapabilitiesGraph `json:"graph,omitempty" xml:"graph,omitempty" mapstructure:"graph"`
|
||||
PasswordPolicy *CapabilitiesPasswordPolicy `json:"password_policy,omitempty" xml:"password_policy,omitempty" mapstructure:"password_policy"`
|
||||
Search *CapabilitiesSearch `json:"search,omitempty" xml:"search,omitempty" mapstructure:"search"`
|
||||
Theme *CapabilitiesTheme `json:"theme,omitempty" xml:"theme,omitempty" mapstructure:"theme"`
|
||||
Notifications *CapabilitiesNotifications `json:"notifications,omitempty" xml:"notifications,omitempty"`
|
||||
}
|
||||
|
||||
@@ -290,6 +291,17 @@ type CapabilitiesNotifications struct {
|
||||
Endpoints []string `json:"ocs-endpoints,omitempty" xml:"ocs-endpoints>element,omitempty" mapstructure:"endpoints"`
|
||||
}
|
||||
|
||||
// CapabilitiesTheme holds theming capabilities
|
||||
type CapabilitiesTheme struct {
|
||||
Logo *CapabilitiesThemeLogo `json:"logo" xml:"logo" mapstructure:"logo"`
|
||||
}
|
||||
|
||||
// CapabilitiesThemeLogo holds theming logo capabilities
|
||||
type CapabilitiesThemeLogo struct {
|
||||
// xml marshal, unmarshal does not support map[string]string, needs a custom type with MarshalXML and UnmarshalXML implementations
|
||||
PermittedFileTypes map[string]string `json:"permitted_file_types" xml:"-" mapstructure:"permitted_file_types"`
|
||||
}
|
||||
|
||||
// Version holds version information
|
||||
type Version struct {
|
||||
Major int `json:"major" xml:"major"`
|
||||
21
vendor/github.com/tidwall/sjson/LICENSE
generated
vendored
Normal file
21
vendor/github.com/tidwall/sjson/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Josh Baker
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
278
vendor/github.com/tidwall/sjson/README.md
generated
vendored
Normal file
278
vendor/github.com/tidwall/sjson/README.md
generated
vendored
Normal file
@@ -0,0 +1,278 @@
|
||||
<p align="center">
|
||||
<img
|
||||
src="logo.png"
|
||||
width="240" height="78" border="0" alt="SJSON">
|
||||
<br>
|
||||
<a href="https://godoc.org/github.com/tidwall/sjson"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
||||
</p>
|
||||
|
||||
<p align="center">set a json value quickly</p>
|
||||
|
||||
SJSON is a Go package that provides a [very fast](#performance) and simple way to set a value in a json document.
|
||||
For quickly retrieving json values check out [GJSON](https://github.com/tidwall/gjson).
|
||||
|
||||
For a command line interface check out [JJ](https://github.com/tidwall/jj).
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
To start using SJSON, install Go and run `go get`:
|
||||
|
||||
```sh
|
||||
$ go get -u github.com/tidwall/sjson
|
||||
```
|
||||
|
||||
This will retrieve the library.
|
||||
|
||||
Set a value
|
||||
-----------
|
||||
Set sets the value for the specified path.
|
||||
A path is in dot syntax, such as "name.last" or "age".
|
||||
This function expects that the json is well-formed and validated.
|
||||
Invalid json will not panic, but it may return back unexpected results.
|
||||
Invalid paths may return an error.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "github.com/tidwall/sjson"
|
||||
|
||||
const json = `{"name":{"first":"Janet","last":"Prichard"},"age":47}`
|
||||
|
||||
func main() {
|
||||
value, _ := sjson.Set(json, "name.last", "Anderson")
|
||||
println(value)
|
||||
}
|
||||
```
|
||||
|
||||
This will print:
|
||||
|
||||
```json
|
||||
{"name":{"first":"Janet","last":"Anderson"},"age":47}
|
||||
```
|
||||
|
||||
Path syntax
|
||||
-----------
|
||||
|
||||
A path is a series of keys separated by a dot.
|
||||
The dot and colon characters can be escaped with ``\``.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": {"first": "Tom", "last": "Anderson"},
|
||||
"age":37,
|
||||
"children": ["Sara","Alex","Jack"],
|
||||
"fav.movie": "Deer Hunter",
|
||||
"friends": [
|
||||
{"first": "James", "last": "Murphy"},
|
||||
{"first": "Roger", "last": "Craig"}
|
||||
]
|
||||
}
|
||||
```
|
||||
```
|
||||
"name.last" >> "Anderson"
|
||||
"age" >> 37
|
||||
"children.1" >> "Alex"
|
||||
"friends.1.last" >> "Craig"
|
||||
```
|
||||
|
||||
The `-1` key can be used to append a value to an existing array:
|
||||
|
||||
```
|
||||
"children.-1" >> appends a new value to the end of the children array
|
||||
```
|
||||
|
||||
Normally number keys are used to modify arrays, but it's possible to force a numeric object key by using the colon character:
|
||||
|
||||
```json
|
||||
{
|
||||
"users":{
|
||||
"2313":{"name":"Sara"},
|
||||
"7839":{"name":"Andy"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
A colon path would look like:
|
||||
|
||||
```
|
||||
"users.:2313.name" >> "Sara"
|
||||
```
|
||||
|
||||
Supported types
|
||||
---------------
|
||||
|
||||
Pretty much any type is supported:
|
||||
|
||||
```go
|
||||
sjson.Set(`{"key":true}`, "key", nil)
|
||||
sjson.Set(`{"key":true}`, "key", false)
|
||||
sjson.Set(`{"key":true}`, "key", 1)
|
||||
sjson.Set(`{"key":true}`, "key", 10.5)
|
||||
sjson.Set(`{"key":true}`, "key", "hello")
|
||||
sjson.Set(`{"key":true}`, "key", []string{"hello", "world"})
|
||||
sjson.Set(`{"key":true}`, "key", map[string]interface{}{"hello":"world"})
|
||||
```
|
||||
|
||||
When a type is not recognized, SJSON will fallback to the `encoding/json` Marshaller.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
Set a value from empty document:
|
||||
```go
|
||||
value, _ := sjson.Set("", "name", "Tom")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":"Tom"}
|
||||
```
|
||||
|
||||
Set a nested value from empty document:
|
||||
```go
|
||||
value, _ := sjson.Set("", "name.last", "Anderson")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"last":"Anderson"}}
|
||||
```
|
||||
|
||||
Set a new value:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.first", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"first":"Sara","last":"Anderson"}}
|
||||
```
|
||||
|
||||
Update an existing value:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"name":{"last":"Anderson"}}`, "name.last", "Smith")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"last":"Smith"}}
|
||||
```
|
||||
|
||||
Set a new array value:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.2", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy","Carol","Sara"]
|
||||
```
|
||||
|
||||
Append an array value by using the `-1` key in a path:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.-1", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy","Carol","Sara"]
|
||||
```
|
||||
|
||||
Append an array value that is past the end:
|
||||
```go
|
||||
value, _ := sjson.Set(`{"friends":["Andy","Carol"]}`, "friends.4", "Sara")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy","Carol",null,null,"Sara"]
|
||||
```
|
||||
|
||||
Delete a value:
|
||||
```go
|
||||
value, _ := sjson.Delete(`{"name":{"first":"Sara","last":"Anderson"}}`, "name.first")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"name":{"last":"Anderson"}}
|
||||
```
|
||||
|
||||
Delete an array value:
|
||||
```go
|
||||
value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.1")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy"]}
|
||||
```
|
||||
|
||||
Delete the last array value:
|
||||
```go
|
||||
value, _ := sjson.Delete(`{"friends":["Andy","Carol"]}`, "friends.-1")
|
||||
println(value)
|
||||
|
||||
// Output:
|
||||
// {"friends":["Andy"]}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarks of SJSON alongside [encoding/json](https://golang.org/pkg/encoding/json/),
|
||||
[ffjson](https://github.com/pquerna/ffjson),
|
||||
[EasyJSON](https://github.com/mailru/easyjson),
|
||||
and [Gabs](https://github.com/Jeffail/gabs)
|
||||
|
||||
```
|
||||
Benchmark_SJSON-8 3000000 805 ns/op 1077 B/op 3 allocs/op
|
||||
Benchmark_SJSON_ReplaceInPlace-8 3000000 449 ns/op 0 B/op 0 allocs/op
|
||||
Benchmark_JSON_Map-8 300000 21236 ns/op 6392 B/op 150 allocs/op
|
||||
Benchmark_JSON_Struct-8 300000 14691 ns/op 1789 B/op 24 allocs/op
|
||||
Benchmark_Gabs-8 300000 21311 ns/op 6752 B/op 150 allocs/op
|
||||
Benchmark_FFJSON-8 300000 17673 ns/op 3589 B/op 47 allocs/op
|
||||
Benchmark_EasyJSON-8 1500000 3119 ns/op 1061 B/op 13 allocs/op
|
||||
```
|
||||
|
||||
JSON document used:
|
||||
|
||||
```json
|
||||
{
|
||||
"widget": {
|
||||
"debug": "on",
|
||||
"window": {
|
||||
"title": "Sample Konfabulator Widget",
|
||||
"name": "main_window",
|
||||
"width": 500,
|
||||
"height": 500
|
||||
},
|
||||
"image": {
|
||||
"src": "Images/Sun.png",
|
||||
"hOffset": 250,
|
||||
"vOffset": 250,
|
||||
"alignment": "center"
|
||||
},
|
||||
"text": {
|
||||
"data": "Click Here",
|
||||
"size": 36,
|
||||
"style": "bold",
|
||||
"vOffset": 100,
|
||||
"alignment": "center",
|
||||
"onMouseUp": "sun1.opacity = (sun1.opacity / 100) * 90;"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each operation was rotated though one of the following search paths:
|
||||
|
||||
```
|
||||
widget.window.name
|
||||
widget.image.hOffset
|
||||
widget.text.onMouseUp
|
||||
```
|
||||
|
||||
*These benchmarks were run on a MacBook Pro 15" 2.8 GHz Intel Core i7 using Go 1.7 and can be be found [here](https://github.com/tidwall/sjson-benchmarks)*.
|
||||
|
||||
## Contact
|
||||
Josh Baker [@tidwall](http://twitter.com/tidwall)
|
||||
|
||||
## License
|
||||
|
||||
SJSON source code is available under the MIT [License](/LICENSE).
|
||||
BIN
vendor/github.com/tidwall/sjson/logo.png
generated
vendored
Normal file
BIN
vendor/github.com/tidwall/sjson/logo.png
generated
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
737
vendor/github.com/tidwall/sjson/sjson.go
generated
vendored
Normal file
737
vendor/github.com/tidwall/sjson/sjson.go
generated
vendored
Normal file
@@ -0,0 +1,737 @@
|
||||
// Package sjson provides setting json values.
|
||||
package sjson
|
||||
|
||||
import (
|
||||
jsongo "encoding/json"
|
||||
"sort"
|
||||
"strconv"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
type errorType struct {
|
||||
msg string
|
||||
}
|
||||
|
||||
func (err *errorType) Error() string {
|
||||
return err.msg
|
||||
}
|
||||
|
||||
// Options represents additional options for the Set and Delete functions.
|
||||
type Options struct {
|
||||
// Optimistic is a hint that the value likely exists which
|
||||
// allows for the sjson to perform a fast-track search and replace.
|
||||
Optimistic bool
|
||||
// ReplaceInPlace is a hint to replace the input json rather than
|
||||
// allocate a new json byte slice. When this field is specified
|
||||
// the input json will not longer be valid and it should not be used
|
||||
// In the case when the destination slice doesn't have enough free
|
||||
// bytes to replace the data in place, a new bytes slice will be
|
||||
// created under the hood.
|
||||
// The Optimistic flag must be set to true and the input must be a
|
||||
// byte slice in order to use this field.
|
||||
ReplaceInPlace bool
|
||||
}
|
||||
|
||||
type pathResult struct {
|
||||
part string // current key part
|
||||
gpart string // gjson get part
|
||||
path string // remaining path
|
||||
force bool // force a string key
|
||||
more bool // there is more path to parse
|
||||
}
|
||||
|
||||
func isSimpleChar(ch byte) bool {
|
||||
switch ch {
|
||||
case '|', '#', '@', '*', '?':
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func parsePath(path string) (res pathResult, simple bool) {
|
||||
var r pathResult
|
||||
if len(path) > 0 && path[0] == ':' {
|
||||
r.force = true
|
||||
path = path[1:]
|
||||
}
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] == '.' {
|
||||
r.part = path[:i]
|
||||
r.gpart = path[:i]
|
||||
r.path = path[i+1:]
|
||||
r.more = true
|
||||
return r, true
|
||||
}
|
||||
if !isSimpleChar(path[i]) {
|
||||
return r, false
|
||||
}
|
||||
if path[i] == '\\' {
|
||||
// go into escape mode. this is a slower path that
|
||||
// strips off the escape character from the part.
|
||||
epart := []byte(path[:i])
|
||||
gpart := []byte(path[:i+1])
|
||||
i++
|
||||
if i < len(path) {
|
||||
epart = append(epart, path[i])
|
||||
gpart = append(gpart, path[i])
|
||||
i++
|
||||
for ; i < len(path); i++ {
|
||||
if path[i] == '\\' {
|
||||
gpart = append(gpart, '\\')
|
||||
i++
|
||||
if i < len(path) {
|
||||
epart = append(epart, path[i])
|
||||
gpart = append(gpart, path[i])
|
||||
}
|
||||
continue
|
||||
} else if path[i] == '.' {
|
||||
r.part = string(epart)
|
||||
r.gpart = string(gpart)
|
||||
r.path = path[i+1:]
|
||||
r.more = true
|
||||
return r, true
|
||||
} else if !isSimpleChar(path[i]) {
|
||||
return r, false
|
||||
}
|
||||
epart = append(epart, path[i])
|
||||
gpart = append(gpart, path[i])
|
||||
}
|
||||
}
|
||||
// append the last part
|
||||
r.part = string(epart)
|
||||
r.gpart = string(gpart)
|
||||
return r, true
|
||||
}
|
||||
}
|
||||
r.part = path
|
||||
r.gpart = path
|
||||
return r, true
|
||||
}
|
||||
|
||||
func mustMarshalString(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// appendStringify makes a json string and appends to buf.
|
||||
func appendStringify(buf []byte, s string) []byte {
|
||||
if mustMarshalString(s) {
|
||||
b, _ := jsongo.Marshal(s)
|
||||
return append(buf, b...)
|
||||
}
|
||||
buf = append(buf, '"')
|
||||
buf = append(buf, s...)
|
||||
buf = append(buf, '"')
|
||||
return buf
|
||||
}
|
||||
|
||||
// appendBuild builds a json block from a json path.
|
||||
func appendBuild(buf []byte, array bool, paths []pathResult, raw string,
|
||||
stringify bool) []byte {
|
||||
if !array {
|
||||
buf = appendStringify(buf, paths[0].part)
|
||||
buf = append(buf, ':')
|
||||
}
|
||||
if len(paths) > 1 {
|
||||
n, numeric := atoui(paths[1])
|
||||
if numeric || (!paths[1].force && paths[1].part == "-1") {
|
||||
buf = append(buf, '[')
|
||||
buf = appendRepeat(buf, "null,", n)
|
||||
buf = appendBuild(buf, true, paths[1:], raw, stringify)
|
||||
buf = append(buf, ']')
|
||||
} else {
|
||||
buf = append(buf, '{')
|
||||
buf = appendBuild(buf, false, paths[1:], raw, stringify)
|
||||
buf = append(buf, '}')
|
||||
}
|
||||
} else {
|
||||
if stringify {
|
||||
buf = appendStringify(buf, raw)
|
||||
} else {
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// atoui does a rip conversion of string -> unigned int.
|
||||
func atoui(r pathResult) (n int, ok bool) {
|
||||
if r.force {
|
||||
return 0, false
|
||||
}
|
||||
for i := 0; i < len(r.part); i++ {
|
||||
if r.part[i] < '0' || r.part[i] > '9' {
|
||||
return 0, false
|
||||
}
|
||||
n = n*10 + int(r.part[i]-'0')
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
// appendRepeat repeats string "n" times and appends to buf.
|
||||
func appendRepeat(buf []byte, s string, n int) []byte {
|
||||
for i := 0; i < n; i++ {
|
||||
buf = append(buf, s...)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
// trim does a rip trim
|
||||
func trim(s string) string {
|
||||
for len(s) > 0 {
|
||||
if s[0] <= ' ' {
|
||||
s = s[1:]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
for len(s) > 0 {
|
||||
if s[len(s)-1] <= ' ' {
|
||||
s = s[:len(s)-1]
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// deleteTailItem deletes the previous key or comma.
|
||||
func deleteTailItem(buf []byte) ([]byte, bool) {
|
||||
loop:
|
||||
for i := len(buf) - 1; i >= 0; i-- {
|
||||
// look for either a ',',':','['
|
||||
switch buf[i] {
|
||||
case '[':
|
||||
return buf, true
|
||||
case ',':
|
||||
return buf[:i], false
|
||||
case ':':
|
||||
// delete tail string
|
||||
i--
|
||||
for ; i >= 0; i-- {
|
||||
if buf[i] == '"' {
|
||||
i--
|
||||
for ; i >= 0; i-- {
|
||||
if buf[i] == '"' {
|
||||
i--
|
||||
if i >= 0 && buf[i] == '\\' {
|
||||
i--
|
||||
continue
|
||||
}
|
||||
for ; i >= 0; i-- {
|
||||
// look for either a ',','{'
|
||||
switch buf[i] {
|
||||
case '{':
|
||||
return buf[:i+1], true
|
||||
case ',':
|
||||
return buf[:i], false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
break loop
|
||||
}
|
||||
}
|
||||
return buf, false
|
||||
}
|
||||
|
||||
var errNoChange = &errorType{"no change"}
|
||||
|
||||
func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string,
|
||||
stringify, del bool) ([]byte, error) {
|
||||
var err error
|
||||
var res gjson.Result
|
||||
var found bool
|
||||
if del {
|
||||
if paths[0].part == "-1" && !paths[0].force {
|
||||
res = gjson.Get(jstr, "#")
|
||||
if res.Int() > 0 {
|
||||
res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10))
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
res = gjson.Get(jstr, paths[0].gpart)
|
||||
}
|
||||
if res.Index > 0 {
|
||||
if len(paths) > 1 {
|
||||
buf = append(buf, jstr[:res.Index]...)
|
||||
buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw,
|
||||
stringify, del)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, jstr[res.Index+len(res.Raw):]...)
|
||||
return buf, nil
|
||||
}
|
||||
buf = append(buf, jstr[:res.Index]...)
|
||||
var exidx int // additional forward stripping
|
||||
if del {
|
||||
var delNextComma bool
|
||||
buf, delNextComma = deleteTailItem(buf)
|
||||
if delNextComma {
|
||||
i, j := res.Index+len(res.Raw), 0
|
||||
for ; i < len(jstr); i, j = i+1, j+1 {
|
||||
if jstr[i] <= ' ' {
|
||||
continue
|
||||
}
|
||||
if jstr[i] == ',' {
|
||||
exidx = j + 1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if stringify {
|
||||
buf = appendStringify(buf, raw)
|
||||
} else {
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
}
|
||||
buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...)
|
||||
return buf, nil
|
||||
}
|
||||
if del {
|
||||
return nil, errNoChange
|
||||
}
|
||||
n, numeric := atoui(paths[0])
|
||||
isempty := true
|
||||
for i := 0; i < len(jstr); i++ {
|
||||
if jstr[i] > ' ' {
|
||||
isempty = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isempty {
|
||||
if numeric {
|
||||
jstr = "[]"
|
||||
} else {
|
||||
jstr = "{}"
|
||||
}
|
||||
}
|
||||
jsres := gjson.Parse(jstr)
|
||||
if jsres.Type != gjson.JSON {
|
||||
if numeric {
|
||||
jstr = "[]"
|
||||
} else {
|
||||
jstr = "{}"
|
||||
}
|
||||
jsres = gjson.Parse(jstr)
|
||||
}
|
||||
var comma bool
|
||||
for i := 1; i < len(jsres.Raw); i++ {
|
||||
if jsres.Raw[i] <= ' ' {
|
||||
continue
|
||||
}
|
||||
if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' {
|
||||
break
|
||||
}
|
||||
comma = true
|
||||
break
|
||||
}
|
||||
switch jsres.Raw[0] {
|
||||
default:
|
||||
return nil, &errorType{"json must be an object or array"}
|
||||
case '{':
|
||||
end := len(jsres.Raw) - 1
|
||||
for ; end > 0; end-- {
|
||||
if jsres.Raw[end] == '}' {
|
||||
break
|
||||
}
|
||||
}
|
||||
buf = append(buf, jsres.Raw[:end]...)
|
||||
if comma {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
buf = appendBuild(buf, false, paths, raw, stringify)
|
||||
buf = append(buf, '}')
|
||||
return buf, nil
|
||||
case '[':
|
||||
var appendit bool
|
||||
if !numeric {
|
||||
if paths[0].part == "-1" && !paths[0].force {
|
||||
appendit = true
|
||||
} else {
|
||||
return nil, &errorType{
|
||||
"cannot set array element for non-numeric key '" +
|
||||
paths[0].part + "'"}
|
||||
}
|
||||
}
|
||||
if appendit {
|
||||
njson := trim(jsres.Raw)
|
||||
if njson[len(njson)-1] == ']' {
|
||||
njson = njson[:len(njson)-1]
|
||||
}
|
||||
buf = append(buf, njson...)
|
||||
if comma {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
|
||||
buf = appendBuild(buf, true, paths, raw, stringify)
|
||||
buf = append(buf, ']')
|
||||
return buf, nil
|
||||
}
|
||||
buf = append(buf, '[')
|
||||
ress := jsres.Array()
|
||||
for i := 0; i < len(ress); i++ {
|
||||
if i > 0 {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
buf = append(buf, ress[i].Raw...)
|
||||
}
|
||||
if len(ress) == 0 {
|
||||
buf = appendRepeat(buf, "null,", n-len(ress))
|
||||
} else {
|
||||
buf = appendRepeat(buf, ",null", n-len(ress))
|
||||
if comma {
|
||||
buf = append(buf, ',')
|
||||
}
|
||||
}
|
||||
buf = appendBuild(buf, true, paths, raw, stringify)
|
||||
buf = append(buf, ']')
|
||||
return buf, nil
|
||||
}
|
||||
}
|
||||
|
||||
func isOptimisticPath(path string) bool {
|
||||
for i := 0; i < len(path); i++ {
|
||||
if path[i] < '.' || path[i] > 'z' {
|
||||
return false
|
||||
}
|
||||
if path[i] > '9' && path[i] < 'A' {
|
||||
return false
|
||||
}
|
||||
if path[i] > 'z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Set sets a json value for the specified path.
|
||||
// A path is in dot syntax, such as "name.last" or "age".
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
// An error is returned if the path is not valid.
|
||||
//
|
||||
// A path is a series of keys separated by a dot.
|
||||
//
|
||||
// {
|
||||
// "name": {"first": "Tom", "last": "Anderson"},
|
||||
// "age":37,
|
||||
// "children": ["Sara","Alex","Jack"],
|
||||
// "friends": [
|
||||
// {"first": "James", "last": "Murphy"},
|
||||
// {"first": "Roger", "last": "Craig"}
|
||||
// ]
|
||||
// }
|
||||
// "name.last" >> "Anderson"
|
||||
// "age" >> 37
|
||||
// "children.1" >> "Alex"
|
||||
//
|
||||
func Set(json, path string, value interface{}) (string, error) {
|
||||
return SetOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
// SetBytes sets a json value for the specified path.
|
||||
// If working with bytes, this method preferred over
|
||||
// Set(string(data), path, value)
|
||||
func SetBytes(json []byte, path string, value interface{}) ([]byte, error) {
|
||||
return SetBytesOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
// SetRaw sets a raw json value for the specified path.
|
||||
// This function works the same as Set except that the value is set as a
|
||||
// raw block of json. This allows for setting premarshalled json objects.
|
||||
func SetRaw(json, path, value string) (string, error) {
|
||||
return SetRawOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
// SetRawOptions sets a raw json value for the specified path with options.
|
||||
// This furnction works the same as SetOptions except that the value is set
|
||||
// as a raw block of json. This allows for setting premarshalled json objects.
|
||||
func SetRawOptions(json, path, value string, opts *Options) (string, error) {
|
||||
var optimistic bool
|
||||
if opts != nil {
|
||||
optimistic = opts.Optimistic
|
||||
}
|
||||
res, err := set(json, path, value, false, false, optimistic, false)
|
||||
if err == errNoChange {
|
||||
return json, nil
|
||||
}
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// SetRawBytes sets a raw json value for the specified path.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetRaw(string(data), path, value)
|
||||
func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) {
|
||||
return SetRawBytesOptions(json, path, value, nil)
|
||||
}
|
||||
|
||||
type dtype struct{}
|
||||
|
||||
// Delete deletes a value from json for the specified path.
|
||||
func Delete(json, path string) (string, error) {
|
||||
return Set(json, path, dtype{})
|
||||
}
|
||||
|
||||
// DeleteBytes deletes a value from json for the specified path.
|
||||
func DeleteBytes(json []byte, path string) ([]byte, error) {
|
||||
return SetBytes(json, path, dtype{})
|
||||
}
|
||||
|
||||
type stringHeader struct {
|
||||
data unsafe.Pointer
|
||||
len int
|
||||
}
|
||||
|
||||
type sliceHeader struct {
|
||||
data unsafe.Pointer
|
||||
len int
|
||||
cap int
|
||||
}
|
||||
|
||||
func set(jstr, path, raw string,
|
||||
stringify, del, optimistic, inplace bool) ([]byte, error) {
|
||||
if path == "" {
|
||||
return []byte(jstr), &errorType{"path cannot be empty"}
|
||||
}
|
||||
if !del && optimistic && isOptimisticPath(path) {
|
||||
res := gjson.Get(jstr, path)
|
||||
if res.Exists() && res.Index > 0 {
|
||||
sz := len(jstr) - len(res.Raw) + len(raw)
|
||||
if stringify {
|
||||
sz += 2
|
||||
}
|
||||
if inplace && sz <= len(jstr) {
|
||||
if !stringify || !mustMarshalString(raw) {
|
||||
jsonh := *(*stringHeader)(unsafe.Pointer(&jstr))
|
||||
jsonbh := sliceHeader{
|
||||
data: jsonh.data, len: jsonh.len, cap: jsonh.len}
|
||||
jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh))
|
||||
if stringify {
|
||||
jbytes[res.Index] = '"'
|
||||
copy(jbytes[res.Index+1:], []byte(raw))
|
||||
jbytes[res.Index+1+len(raw)] = '"'
|
||||
copy(jbytes[res.Index+1+len(raw)+1:],
|
||||
jbytes[res.Index+len(res.Raw):])
|
||||
} else {
|
||||
copy(jbytes[res.Index:], []byte(raw))
|
||||
copy(jbytes[res.Index+len(raw):],
|
||||
jbytes[res.Index+len(res.Raw):])
|
||||
}
|
||||
return jbytes[:sz], nil
|
||||
}
|
||||
return []byte(jstr), nil
|
||||
}
|
||||
buf := make([]byte, 0, sz)
|
||||
buf = append(buf, jstr[:res.Index]...)
|
||||
if stringify {
|
||||
buf = appendStringify(buf, raw)
|
||||
} else {
|
||||
buf = append(buf, raw...)
|
||||
}
|
||||
buf = append(buf, jstr[res.Index+len(res.Raw):]...)
|
||||
return buf, nil
|
||||
}
|
||||
}
|
||||
var paths []pathResult
|
||||
r, simple := parsePath(path)
|
||||
if simple {
|
||||
paths = append(paths, r)
|
||||
for r.more {
|
||||
r, simple = parsePath(r.path)
|
||||
if !simple {
|
||||
break
|
||||
}
|
||||
paths = append(paths, r)
|
||||
}
|
||||
}
|
||||
if !simple {
|
||||
if del {
|
||||
return []byte(jstr),
|
||||
&errorType{"cannot delete value from a complex path"}
|
||||
}
|
||||
return setComplexPath(jstr, path, raw, stringify)
|
||||
}
|
||||
njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del)
|
||||
if err != nil {
|
||||
return []byte(jstr), err
|
||||
}
|
||||
return njson, nil
|
||||
}
|
||||
|
||||
func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) {
|
||||
res := gjson.Get(jstr, path)
|
||||
if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) {
|
||||
return []byte(jstr), errNoChange
|
||||
}
|
||||
if res.Index != 0 {
|
||||
njson := []byte(jstr[:res.Index])
|
||||
if stringify {
|
||||
njson = appendStringify(njson, raw)
|
||||
} else {
|
||||
njson = append(njson, raw...)
|
||||
}
|
||||
njson = append(njson, jstr[res.Index+len(res.Raw):]...)
|
||||
jstr = string(njson)
|
||||
}
|
||||
if len(res.Indexes) > 0 {
|
||||
type val struct {
|
||||
index int
|
||||
res gjson.Result
|
||||
}
|
||||
vals := make([]val, 0, len(res.Indexes))
|
||||
res.ForEach(func(_, vres gjson.Result) bool {
|
||||
vals = append(vals, val{res: vres})
|
||||
return true
|
||||
})
|
||||
if len(res.Indexes) != len(vals) {
|
||||
return []byte(jstr), errNoChange
|
||||
}
|
||||
for i := 0; i < len(res.Indexes); i++ {
|
||||
vals[i].index = res.Indexes[i]
|
||||
}
|
||||
sort.SliceStable(vals, func(i, j int) bool {
|
||||
return vals[i].index > vals[j].index
|
||||
})
|
||||
for _, val := range vals {
|
||||
vres := val.res
|
||||
index := val.index
|
||||
njson := []byte(jstr[:index])
|
||||
if stringify {
|
||||
njson = appendStringify(njson, raw)
|
||||
} else {
|
||||
njson = append(njson, raw...)
|
||||
}
|
||||
njson = append(njson, jstr[index+len(vres.Raw):]...)
|
||||
jstr = string(njson)
|
||||
}
|
||||
}
|
||||
return []byte(jstr), nil
|
||||
}
|
||||
|
||||
// SetOptions sets a json value for the specified path with options.
|
||||
// A path is in dot syntax, such as "name.last" or "age".
|
||||
// This function expects that the json is well-formed, and does not validate.
|
||||
// Invalid json will not panic, but it may return back unexpected results.
|
||||
// An error is returned if the path is not valid.
|
||||
func SetOptions(json, path string, value interface{},
|
||||
opts *Options) (string, error) {
|
||||
if opts != nil {
|
||||
if opts.ReplaceInPlace {
|
||||
// it's not safe to replace bytes in-place for strings
|
||||
// copy the Options and set options.ReplaceInPlace to false.
|
||||
nopts := *opts
|
||||
opts = &nopts
|
||||
opts.ReplaceInPlace = false
|
||||
}
|
||||
}
|
||||
jsonh := *(*stringHeader)(unsafe.Pointer(&json))
|
||||
jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len}
|
||||
jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh))
|
||||
res, err := SetBytesOptions(jsonb, path, value, opts)
|
||||
return string(res), err
|
||||
}
|
||||
|
||||
// SetBytesOptions sets a json value for the specified path with options.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetOptions(string(data), path, value)
|
||||
func SetBytesOptions(json []byte, path string, value interface{},
|
||||
opts *Options) ([]byte, error) {
|
||||
var optimistic, inplace bool
|
||||
if opts != nil {
|
||||
optimistic = opts.Optimistic
|
||||
inplace = opts.ReplaceInPlace
|
||||
}
|
||||
jstr := *(*string)(unsafe.Pointer(&json))
|
||||
var res []byte
|
||||
var err error
|
||||
switch v := value.(type) {
|
||||
default:
|
||||
b, merr := jsongo.Marshal(value)
|
||||
if merr != nil {
|
||||
return nil, merr
|
||||
}
|
||||
raw := *(*string)(unsafe.Pointer(&b))
|
||||
res, err = set(jstr, path, raw, false, false, optimistic, inplace)
|
||||
case dtype:
|
||||
res, err = set(jstr, path, "", false, true, optimistic, inplace)
|
||||
case string:
|
||||
res, err = set(jstr, path, v, true, false, optimistic, inplace)
|
||||
case []byte:
|
||||
raw := *(*string)(unsafe.Pointer(&v))
|
||||
res, err = set(jstr, path, raw, true, false, optimistic, inplace)
|
||||
case bool:
|
||||
if v {
|
||||
res, err = set(jstr, path, "true", false, false, optimistic, inplace)
|
||||
} else {
|
||||
res, err = set(jstr, path, "false", false, false, optimistic, inplace)
|
||||
}
|
||||
case int8:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case int16:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case int32:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case int64:
|
||||
res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint8:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint16:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint32:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case uint64:
|
||||
res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
|
||||
false, false, optimistic, inplace)
|
||||
case float32:
|
||||
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
|
||||
false, false, optimistic, inplace)
|
||||
case float64:
|
||||
res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
|
||||
false, false, optimistic, inplace)
|
||||
}
|
||||
if err == errNoChange {
|
||||
return json, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
|
||||
// SetRawBytesOptions sets a raw json value for the specified path with options.
|
||||
// If working with bytes, this method preferred over
|
||||
// SetRawOptions(string(data), path, value, opts)
|
||||
func SetRawBytesOptions(json []byte, path string, value []byte,
|
||||
opts *Options) ([]byte, error) {
|
||||
jstr := *(*string)(unsafe.Pointer(&json))
|
||||
vstr := *(*string)(unsafe.Pointer(&value))
|
||||
var optimistic, inplace bool
|
||||
if opts != nil {
|
||||
optimistic = opts.Optimistic
|
||||
inplace = opts.ReplaceInPlace
|
||||
}
|
||||
res, err := set(jstr, path, vstr, false, false, optimistic, inplace)
|
||||
if err == errNoChange {
|
||||
return json, nil
|
||||
}
|
||||
return res, err
|
||||
}
|
||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@@ -366,7 +366,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
|
||||
github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
|
||||
github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
|
||||
github.com/cs3org/go-cs3apis/cs3/types/v1beta1
|
||||
# github.com/cs3org/reva/v2 v2.19.2-0.20240525160759-56879111e06a
|
||||
# github.com/cs3org/reva/v2 v2.19.2-0.20240529081036-419196f2342e
|
||||
## explicit; go 1.21
|
||||
github.com/cs3org/reva/v2/cmd/revad/internal/grace
|
||||
github.com/cs3org/reva/v2/cmd/revad/runtime
|
||||
@@ -569,6 +569,7 @@ github.com/cs3org/reva/v2/pkg/ocm/share/repository/loader
|
||||
github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud
|
||||
github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry
|
||||
github.com/cs3org/reva/v2/pkg/ocm/storage/received
|
||||
github.com/cs3org/reva/v2/pkg/owncloud/ocs
|
||||
github.com/cs3org/reva/v2/pkg/password
|
||||
github.com/cs3org/reva/v2/pkg/permission
|
||||
github.com/cs3org/reva/v2/pkg/permission/manager/demo
|
||||
@@ -1790,6 +1791,9 @@ github.com/tidwall/match
|
||||
# github.com/tidwall/pretty v1.2.1
|
||||
## explicit; go 1.16
|
||||
github.com/tidwall/pretty
|
||||
# github.com/tidwall/sjson v1.2.5
|
||||
## explicit; go 1.14
|
||||
github.com/tidwall/sjson
|
||||
# github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208
|
||||
## explicit
|
||||
github.com/toorop/go-dkim
|
||||
|
||||
Reference in New Issue
Block a user