From 3c45f09874bf23640ac97a60c3eeee72d8b16d43 Mon Sep 17 00:00:00 2001 From: Philippe-Adrien Nousse Date: Wed, 8 Aug 2018 16:04:06 +0200 Subject: [PATCH 1/4] #1 OCS Shares API --- example.config.yml | 5 +- gonextcloud.iml | 2 +- gonextcloud_test.go | 24 +++++-- routes.go | 4 +- shares.go | 156 ++++++++++++++++++++++++++++++++++++++++++++ types/errors.go | 2 +- types/responses.go | 20 ++++-- types/shares.go | 53 +++++++++++++++ utils.go | 2 +- 9 files changed, 248 insertions(+), 20 deletions(-) create mode 100644 shares.go create mode 100644 types/shares.go diff --git a/example.config.yml b/example.config.yml index b23a734..d1cc996 100644 --- a/example.config.yml +++ b/example.config.yml @@ -2,9 +2,6 @@ url: https://my.nextcloud.com login: admin password: mypassword app-name: testapp -groups-to-create: - - grp1 - - grp2 - - grp3 +share-folder: /Documents not-existing-user: this-user-should-not-exist not-existing-group: this-group-should-not-exist \ No newline at end of file diff --git a/gonextcloud.iml b/gonextcloud.iml index eacc75a..8021953 100644 --- a/gonextcloud.iml +++ b/gonextcloud.iml @@ -1,9 +1,9 @@ - + \ No newline at end of file diff --git a/gonextcloud_test.go b/gonextcloud_test.go index 156149c..3945afd 100644 --- a/gonextcloud_test.go +++ b/gonextcloud_test.go @@ -20,13 +20,13 @@ var c *Client const password = "somecomplicatedpassword" type Config struct { - URL string `yaml:"url"` - Login string `yaml:"login"` - Password string `yaml:"password"` - AppName string `yaml:"app-name"` - GroupsToCreate []string `yaml:"groups-to-create"` - NotExistingUser string `yaml:"not-existing-user"` - NotExistingGroup string `yaml:"not-existing-group"` + URL string `yaml:"url"` + Login string `yaml:"login"` + Password string `yaml:"password"` + AppName string `yaml:"app-name"` + ShareFolder string `yaml:"share-folder"` + NotExistingUser string `yaml:"not-existing-user"` + NotExistingGroup string `yaml:"not-existing-group"` } // LoadConfig loads the test configuration @@ -307,9 +307,19 @@ func TestUserDelete(t *testing.T) { func TestInvalidBaseRequest(t *testing.T) { c.baseURL = &url.URL{} _, err := c.baseRequest(routes.capabilities, "admin", "invalid", nil, http.MethodGet) + c = nil assert.Error(t, err) } +func TestShareList(t *testing.T) { + if err := initClient(); err != nil { + return + } + s, err := c.SharesList() + assert.Nil(t, err) + assert.NotNil(t, s) +} + func TestLogout(t *testing.T) { err := c.Logout() assert.Nil(t, err) diff --git a/routes.go b/routes.go index 66068b8..b43e9f2 100644 --- a/routes.go +++ b/routes.go @@ -9,17 +9,19 @@ type Routes struct { groups *url.URL apps *url.URL monitor *url.URL + shares *url.URL } const badRequest = 998 var ( - apiPath = &url.URL{Path: "/ocs/v1.php"} + apiPath = &url.URL{Path: "/ocs/v2.php"} routes = Routes{ capabilities: &url.URL{Path: apiPath.Path + "/cloud/capabilities"}, users: &url.URL{Path: apiPath.Path + "/cloud/users"}, groups: &url.URL{Path: apiPath.Path + "/cloud/groups"}, apps: &url.URL{Path: apiPath.Path + "/cloud/apps"}, monitor: &url.URL{Path: apiPath.Path + "/apps/serverinfo/api/v1/info"}, + shares: &url.URL{Path: apiPath.Path + "/apps/files_sharing/api/v1/shares"}, } ) diff --git a/shares.go b/shares.go new file mode 100644 index 0000000..09f61a5 --- /dev/null +++ b/shares.go @@ -0,0 +1,156 @@ +package gonextcloud + +import ( + "fmt" + req "github.com/levigross/grequests" + "github.com/partitio/gonextcloud/types" + "net/http" + "strconv" + "sync" +) + +func (c *Client) SharesList() ([]types.Share, error) { + res, err := c.baseRequest(routes.shares, "", "", nil, http.MethodGet) + if err != nil { + return nil, err + } + var r types.SharesListResponse + res.JSON(&r) + return r.Ocs.Data, nil +} + +func (c *Client) Shares(path string, reshares bool, subfiles bool) ([]types.Share, error) { + ro := &req.RequestOptions{ + Params: map[string]string{ + "path": path, + "reshares": strconv.FormatBool(reshares), + "subfiles": strconv.FormatBool(subfiles), + }, + } + res, err := c.baseRequest(routes.shares, "", "", ro, http.MethodGet) + if err != nil { + return nil, err + } + var r types.SharesListResponse + res.JSON(&r) + return r.Ocs.Data, nil +} + +func (c *Client) Share(shareID string) (types.Share, error) { + res, err := c.baseRequest(routes.shares, shareID, "", nil, http.MethodGet) + if err != nil { + return types.Share{}, err + } + var r types.SharesListResponse + res.JSON(&r) + return r.Ocs.Data[0], nil +} + +func (c *Client) ShareCreate( + path string, + shareType types.ShareType, + permission types.SharePermission, + shareWith string, + publicUpload bool, + password string, +) (types.Share, error) { + + if (shareType == types.UserShare || shareType == types.GroupShare) && shareWith == "" { + return types.Share{}, fmt.Errorf("shareWith cannot be empty for ShareType UserShare or GroupShare") + } + ro := &req.RequestOptions{ + Data: map[string]string{ + "path": path, + "shareType": strconv.Itoa(int(shareType)), + "shareWith": shareWith, + "publicUpload": strconv.FormatBool(publicUpload), + "password": password, + "permissions": strconv.Itoa(int(permission)), + }, + } + res, err := c.baseRequest(routes.shares, "", "", ro, http.MethodPost) + if err != nil { + return types.Share{}, err + } + var r types.SharesResponse + res.JSON(&r) + return r.Ocs.Data, nil +} + +func (c *Client) ShareDelete(shareID int) error { + _, err := c.baseRequest(routes.shares, strconv.Itoa(shareID), "", nil, http.MethodDelete) + return err +} + +// expireDate expireDate expects a well formatted date string, e.g. ‘YYYY-MM-DD’ +func (c *Client) ShareUpdate(shareUpdate types.ShareUpdate) error { + errs := make(chan types.UpdateError) + var wg sync.WaitGroup + wg.Add(4) + go func() { + defer wg.Done() + if err := c.ShareUpdatePassword(shareUpdate.ShareID, shareUpdate.Password); err != nil { + errs <- types.UpdateError{ + Field: "password", + Error: err, + } + } + }() + go func() { + defer wg.Done() + if err := c.ShareUpdateExpireDate(shareUpdate.ShareID, shareUpdate.ExpireDate); err != nil { + errs <- types.UpdateError{ + Field: "expireDate", + Error: err, + } + } + }() + go func() { + defer wg.Done() + if err := c.ShareUpdatePermissions(shareUpdate.ShareID, shareUpdate.Permissions); err != nil { + errs <- types.UpdateError{ + Field: "permissions", + Error: err, + } + } + }() + go func() { + defer wg.Done() + if err := c.ShareUpdatePublicUpload(shareUpdate.ShareID, shareUpdate.PublicUpload); err != nil { + errs <- types.UpdateError{ + Field: "publicUpload", + Error: err, + } + } + }() + go func() { + wg.Wait() + close(errs) + }() + return types.NewUpdateError(errs) +} + +// expireDate expects a well formatted date string, e.g. ‘YYYY-MM-DD’ +func (c *Client) ShareUpdateExpireDate(shareID int, expireDate string) error { + return c.baseShareUpdate(strconv.Itoa(shareID), "expireDate", expireDate) +} + +func (c *Client) ShareUpdatePublicUpload(shareID int, public bool) error { + return c.baseShareUpdate(strconv.Itoa(shareID), "publicUpload", strconv.FormatBool(public)) +} + +func (c *Client) ShareUpdatePassword(shareID int, password string) error { + return c.baseShareUpdate(strconv.Itoa(shareID), "password", password) +} + +func (c *Client) ShareUpdatePermissions(shareID int, permissions types.SharePermission) error { + return c.baseShareUpdate(strconv.Itoa(shareID), "permissions", strconv.Itoa(int(permissions))) +} + +func (c *Client) baseShareUpdate(shareID string, key string, value string) error { + ro := &req.RequestOptions{ + Data: map[string]string{key: value}, + } + _, err := c.baseRequest(routes.shares, shareID, "", ro, http.MethodPut) + return err +} diff --git a/types/errors.go b/types/errors.go index be24610..6677176 100644 --- a/types/errors.go +++ b/types/errors.go @@ -38,7 +38,7 @@ type UserUpdateError struct { func (e *UserUpdateError) Error() string { var errors []string for k, e := range e.Errors { - errors = append(errors, fmt.Sprintf("%s: %s", k, e.Error())) + errors = append(errors, fmt.Sprintf("%s: %v", k, e)) } return strings.Join(errors, ",") } diff --git a/types/responses.go b/types/responses.go index 93b0a2b..d456d5a 100644 --- a/types/responses.go +++ b/types/responses.go @@ -90,11 +90,21 @@ type CapabilitiesResponse struct { type MonitoringResponse struct { Ocs struct { - Meta struct { - Status string `json:"status"` - Statuscode int `json:"statuscode"` - Message string `json:"message"` - } `json:"meta"` + Meta Meta `json:"meta"` Data Monitoring `json:"data"` } `json:"ocs"` } + +type SharesListResponse struct { + Ocs struct { + Meta Meta `json:"meta"` + Data []Share `json:"data"` + } `json:"ocs"` +} + +type SharesResponse struct { + Ocs struct { + Meta Meta `json:"meta"` + Data Share `json:"data"` + } `json:"ocs"` +} diff --git a/types/shares.go b/types/shares.go new file mode 100644 index 0000000..7073e94 --- /dev/null +++ b/types/shares.go @@ -0,0 +1,53 @@ +package types + +type ShareType int +type SharePermission int + +const ( + UserShare ShareType = 0 + GroupShare ShareType = 1 + PublicLinkShare ShareType = 3 + FederatedCloudShare ShareType = 6 + + ReadPermission SharePermission = 1 + UpdatePermission SharePermission = 2 + CreatePermission SharePermission = 4 + DeletePermission SharePermission = 8 + ReSharePermission SharePermission = 16 + AllPermissions SharePermission = 31 +) + +type ShareUpdate struct { + ShareID int + Permissions SharePermission + Password string + PublicUpload bool + ExpireDate string +} + +type Share struct { + ID string `json:"id"` + ShareType int `json:"share_type"` + UIDOwner string `json:"uid_owner"` + DisplaynameOwner string `json:"displayname_owner"` + Permissions int `json:"permissions"` + Stime int `json:"stime"` + Parent interface{} `json:"parent"` + Expiration string `json:"expiration"` + Token string `json:"token"` + UIDFileOwner string `json:"uid_file_owner"` + DisplaynameFileOwner string `json:"displayname_file_owner"` + Path string `json:"path"` + ItemType string `json:"item_type"` + Mimetype string `json:"mimetype"` + StorageID string `json:"storage_id"` + Storage int `json:"storage"` + ItemSource int `json:"item_source"` + FileSource int `json:"file_source"` + FileParent int `json:"file_parent"` + FileTarget string `json:"file_target"` + ShareWith string `json:"share_with"` + ShareWithDisplayname string `json:"share_with_displayname"` + MailSend int `json:"mail_send"` + Tags []string `json:"tags"` +} diff --git a/utils.go b/utils.go index 2e6588a..9d87df2 100644 --- a/utils.go +++ b/utils.go @@ -41,7 +41,7 @@ func (c *Client) baseRequest(route *url.URL, name string, subroute string, ro *r js := res.String() var r types.BaseResponse json.Unmarshal([]byte(js), &r) - if r.Ocs.Meta.Statuscode != 100 { + if r.Ocs.Meta.Statuscode != 200 { err := types.ErrorFromMeta(r.Ocs.Meta) return nil, err } From 359e15bfb2a5a13a0ffb8cc667b57ec9d27eec5f Mon Sep 17 00:00:00 2001 From: Philippe-Adrien Nousse Date: Mon, 3 Sep 2018 12:17:02 +0200 Subject: [PATCH 2/4] Fix #1 and #2 : Added groupfolders API client --- Makefile | 4 +- apps.go | 17 ++--- client.go | 1 - config.ymle | 10 --- gonextcloud_test.go | 150 ++++++++++++++++++++++++++++++++---------- groupfolders.go | 131 ++++++++++++++++++++++++++++++++++++ groups.go | 16 ++--- monitoring.go | 2 +- routes.go | 2 + shares.go | 12 ++-- types/groupfolders.go | 57 ++++++++++++++++ types/responses.go | 21 ++++++ users.go | 32 ++++----- utils.go | 29 ++++---- 14 files changed, 380 insertions(+), 104 deletions(-) delete mode 100644 config.ymle create mode 100644 groupfolders.go create mode 100644 types/groupfolders.go diff --git a/Makefile b/Makefile index 4d0ab83..035216c 100644 --- a/Makefile +++ b/Makefile @@ -11,10 +11,10 @@ lint: ## Lint the files @golint -set_exit_status ${PKG_LIST} test: ## Run unittests - @go test ${PKG_LIST} + @go test -v ${PKG_LIST} race: dep ## Run data race detector - @go test -race ${PKG_LIST} + @go test -v -race ${PKG_LIST} msan: dep ## Run memory sanitizer @go test -msan -short ${PKG_LIST} diff --git a/apps.go b/apps.go index 96393d8..4b22727 100644 --- a/apps.go +++ b/apps.go @@ -8,7 +8,7 @@ import ( //AppList return the list of the Nextcloud Apps func (c *Client) AppList() ([]string, error) { - res, err := c.baseRequest(routes.apps, "", "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.apps, nil) if err != nil { return nil, err } @@ -22,7 +22,7 @@ func (c *Client) AppListEnabled() ([]string, error) { ro := &req.RequestOptions{ Params: map[string]string{"filter": "enabled"}, } - res, err := c.baseRequest(routes.apps, "", "", ro, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.apps, ro) if err != nil { return nil, err } @@ -36,7 +36,7 @@ func (c *Client) AppListDisabled() ([]string, error) { ro := &req.RequestOptions{ Params: map[string]string{"filter": "disabled"}, } - res, err := c.baseRequest(routes.apps, "", "", ro, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.apps, ro) if err != nil { return nil, err } @@ -47,7 +47,7 @@ func (c *Client) AppListDisabled() ([]string, error) { //AppInfos return the app's details func (c *Client) AppInfos(name string) (types.App, error) { - res, err := c.baseRequest(routes.apps, name, "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.apps, nil, name) if err != nil { return types.App{}, err } @@ -58,17 +58,12 @@ func (c *Client) AppInfos(name string) (types.App, error) { //AppEnable enables an app func (c *Client) AppEnable(name string) error { - _, err := c.baseRequest(routes.apps, name, "", nil, http.MethodPut) + _, err := c.baseRequest(http.MethodPut, routes.apps, nil, name) return err } //AppDisable disables an app func (c *Client) AppDisable(name string) error { - _, err := c.baseRequest(routes.apps, name, "", nil, http.MethodDelete) - return err -} - -func (c *Client) appsBaseRequest(name string, route string, ro *req.RequestOptions, method string) error { - _, err := c.baseRequest(routes.apps, name, route, ro, method) + _, err := c.baseRequest(http.MethodDelete, routes.apps, nil, name) return err } diff --git a/client.go b/client.go index a4a8373..6bddc8c 100644 --- a/client.go +++ b/client.go @@ -37,7 +37,6 @@ For example, to list all the Nextcloud's instance users: fmt.Println("Users :", users) } */ - package gonextcloud import ( diff --git a/config.ymle b/config.ymle deleted file mode 100644 index 3848220..0000000 --- a/config.ymle +++ /dev/null @@ -1,10 +0,0 @@ -url: $NEXTCLOUD_URL -login: admin -password: $NEXTCLOUD_PASSWORD -app-name: testapp -groups-to-create: - - grp1 - - grp2 - - grp3 -not-existing-user: this-user-should-not-exist -not-existing-group: this-group-should-not-exist \ No newline at end of file diff --git a/gonextcloud_test.go b/gonextcloud_test.go index 3945afd..afad9f1 100644 --- a/gonextcloud_test.go +++ b/gonextcloud_test.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/fatih/structs" "github.com/partitio/gonextcloud/types" + "github.com/partitio/swarmmanager/libnextcloudpartitio/utils" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" "io/ioutil" @@ -44,13 +45,13 @@ func LoadConfig() error { func TestLoadConfig(t *testing.T) { err := LoadConfig() - assert.Nil(t, err) + assert.NoError(t, err) } func TestClient(t *testing.T) { var err error c, err = NewClient(config.URL) - assert.Nil(t, err, "aie") + assert.NoError(t, err, "aie") } func TestLoginFail(t *testing.T) { @@ -60,19 +61,19 @@ func TestLoginFail(t *testing.T) { func TestLogin(t *testing.T) { err := c.Login(config.Login, config.Password) - assert.Nil(t, err) + assert.NoError(t, err) } func TestUserList(t *testing.T) { us, err := c.UserList() - assert.Nil(t, err) + assert.NoError(t, err) assert.Contains(t, us, config.Login) } func TestExistingUser(t *testing.T) { u, err := c.User(config.Login) - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, u) } @@ -89,13 +90,13 @@ func TestNonExistingUser(t *testing.T) { func TestUserSearch(t *testing.T) { us, err := c.UserSearch(config.Login) - assert.Nil(t, err) + assert.NoError(t, err) assert.Contains(t, us, config.Login) } func TestUserCreate(t *testing.T) { err := c.UserCreate(config.NotExistingUser, password, nil) - assert.Nil(t, err) + assert.NoError(t, err) } func TestUserCreateFull(t *testing.T) { @@ -115,7 +116,7 @@ func TestUserCreateFull(t *testing.T) { err := c.UserCreate(username, password, user) assert.Nil(t, err) u, err := c.User(username) - assert.Nil(t, err) + assert.NoError(t, err) o := structs.Map(user) r := structs.Map(u) for k := range o { @@ -126,7 +127,7 @@ func TestUserCreateFull(t *testing.T) { } // Clean up err = c.UserDelete(u.ID) - assert.Nil(t, err) + assert.NoError(t, err) } func TestUserUpdate(t *testing.T) { @@ -159,23 +160,23 @@ func TestUserUpdate(t *testing.T) { } // Clean up err = c.UserDelete(u.ID) - assert.Nil(t, err) + assert.NoError(t, err) } func TestUserCreateExisting(t *testing.T) { err := c.UserCreate(config.NotExistingUser, password, nil) - assert.NotNil(t, err) + assert.Error(t, err) } func TestGroupList(t *testing.T) { gs, err := c.GroupList() - assert.Nil(t, err) + assert.NoError(t, err) assert.Contains(t, gs, "admin") } func TestGroupCreate(t *testing.T) { err := c.GroupCreate(config.NotExistingGroup) - assert.Nil(t, err) + assert.NoError(t, err) } func TestUserUpdateEmail(t *testing.T) { @@ -183,7 +184,7 @@ func TestUserUpdateEmail(t *testing.T) { err := c.UserUpdateEmail(config.NotExistingUser, email) assert.Nil(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, email, u.Email) } @@ -192,7 +193,7 @@ func TestUserUpdateDisplayName(t *testing.T) { err := c.UserUpdateDisplayName(config.NotExistingUser, displayName) assert.Nil(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, displayName, u.Displayname) } @@ -201,41 +202,41 @@ func TestUserUpdatePhone(t *testing.T) { err := c.UserUpdatePhone(config.NotExistingUser, phone) assert.Nil(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, phone, u.Phone) } func TestUserUpdateAddress(t *testing.T) { address := "Main Street, Galifrey" err := c.UserUpdateAddress(config.NotExistingUser, address) - assert.Nil(t, err) + assert.NoError(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, address, u.Address) } func TestUserUpdateWebSite(t *testing.T) { website := "www.doctor.who" err := c.UserUpdateWebSite(config.NotExistingUser, website) - assert.Nil(t, err) + assert.NoError(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, website, u.Website) } func TestUserUpdateTwitter(t *testing.T) { twitter := "@doctorwho" err := c.UserUpdateTwitter(config.NotExistingUser, twitter) - assert.Nil(t, err) + assert.NoError(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, twitter, u.Twitter) } func TestUserUpdateQuota(t *testing.T) { quota := 1024 * 1024 * 1024 err := c.UserUpdateQuota(config.NotExistingUser, quota) - assert.Nil(t, err) + assert.NoError(t, err) // TODO : Find better verification : A never connected User does not have quota available //u, err := c.User(config.NotExistingUser) //assert.Nil(t, err) @@ -245,20 +246,20 @@ func TestUserUpdateQuota(t *testing.T) { func TestUserUpdatePassword(t *testing.T) { password := "newcomplexpassword" err := c.UserUpdatePassword(config.NotExistingUser, password) - assert.Nil(t, err) + assert.NoError(t, err) } func TestUserGroupAdd(t *testing.T) { err := c.UserGroupAdd(config.NotExistingUser, config.NotExistingGroup) assert.Nil(t, err) gs, err := c.UserGroupList(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Contains(t, gs, config.NotExistingGroup) } func TestUserGroupSubAdminList(t *testing.T) { gs, err := c.UserGroupSubAdminList(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Empty(t, gs) } @@ -266,13 +267,13 @@ func TestUserGroupPromote(t *testing.T) { err := c.UserGroupPromote(config.NotExistingUser, config.NotExistingGroup) assert.Nil(t, err) gs, err := c.UserGroupSubAdminList(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.Contains(t, gs, config.NotExistingGroup) } func TestUserGroupDemote(t *testing.T) { err := c.UserGroupDemote(config.NotExistingUser, config.NotExistingGroup) - assert.Nil(t, err) + assert.NoError(t, err) //gs, err := c.UserGroupSubAdminList(config.NotExistingUser) //assert.Nil(t, err) //assert.Empty(t, gs) @@ -282,7 +283,7 @@ func TestUserDisable(t *testing.T) { err := c.UserDisable(config.NotExistingUser) assert.Nil(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.False(t, u.Enabled) } @@ -290,7 +291,7 @@ func TestUserEnable(t *testing.T) { err := c.UserEnable(config.NotExistingUser) assert.Nil(t, err) u, err := c.User(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) assert.True(t, u.Enabled) } @@ -301,12 +302,12 @@ func TestGroupDelete(t *testing.T) { func TestUserDelete(t *testing.T) { err := c.UserDelete(config.NotExistingUser) - assert.Nil(t, err) + assert.NoError(t, err) } func TestInvalidBaseRequest(t *testing.T) { c.baseURL = &url.URL{} - _, err := c.baseRequest(routes.capabilities, "admin", "invalid", nil, http.MethodGet) + _, err := c.baseRequest(http.MethodGet, routes.capabilities, nil, "admin", "invalid") c = nil assert.Error(t, err) } @@ -316,13 +317,13 @@ func TestShareList(t *testing.T) { return } s, err := c.SharesList() - assert.Nil(t, err) + assert.NoError(t, err) assert.NotNil(t, s) } func TestLogout(t *testing.T) { err := c.Logout() - assert.Nil(t, err) + assert.NoError(t, err) assert.Nil(t, c.session.HTTPClient.Jar) } @@ -340,10 +341,89 @@ func TestLoginInvalidURL(t *testing.T) { func TestBaseRequest(t *testing.T) { c, _ = NewClient("") - _, err := c.baseRequest(routes.capabilities, "admin", "invalid", nil, http.MethodGet) + _, err := c.baseRequest(http.MethodGet, routes.capabilities, nil, "admin", "invalid") assert.Error(t, err) } +var groupID = 37 + +func TestGroupFoldersCreate(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + var err error + groupID, err = c.GroupFoldersCreate("API") + assert.NoError(t, err) +} + +func TestGroupFoldersList(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + gfs, err := c.GroupFoldersList() + assert.NoError(t, err) + utils.PrettyPrint(gfs) + assert.NotNil(t, gfs[groupID]) +} + +func TestGroupFolders(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + gf, err := c.GroupFolders(groupID) + assert.NoError(t, err) + utils.PrettyPrint(gf) + assert.NotNil(t, gf) +} + +func TestGroupFolderRename(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + err := c.GroupFoldersRename(groupID, "API_Renamed") + assert.NoError(t, err) +} + +func TestGroupFoldersAddGroup(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + err := c.GroupFoldersAddGroup(groupID, "admin") + assert.NoError(t, err) +} + +func TestGroupFoldersSetGroupPermissions(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + err := c.GroupFoldersSetGroupPermissions(groupID, "admin", types.ReadPermission) + assert.NoError(t, err) +} + +func TestGroupFoldersSetQuota(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + err := c.GroupFoldersSetQuota(groupID, 100) + assert.NoError(t, err) +} + +func TestGroupFolderRemoveGroup(t *testing.T) { + c = nil + if err := initClient(); err != nil { + return + } + err := c.GroupFoldersRemoveGroup(groupID, "admin") + assert.NoError(t, err) +} + func initClient() error { if c == nil { if err := LoadConfig(); err != nil { diff --git a/groupfolders.go b/groupfolders.go new file mode 100644 index 0000000..9e6ec43 --- /dev/null +++ b/groupfolders.go @@ -0,0 +1,131 @@ +package gonextcloud + +import ( + "fmt" + req "github.com/levigross/grequests" + "github.com/partitio/gonextcloud/types" + "net/http" + "strconv" +) + +func (c *Client) GroupFoldersList() (map[int]types.GroupFolder, error) { + res, err := c.baseRequest(http.MethodGet, routes.groupfolders, nil) + if err != nil { + return nil, err + } + var r types.GroupFoldersListResponse + res.JSON(&r) + gfs := formatBadIDAndGroups(r.Ocs.Data) + return gfs, nil +} + +func (c *Client) GroupFolders(id int) (types.GroupFolder, error) { + res, err := c.baseRequest(http.MethodGet, routes.groupfolders, nil, strconv.Itoa(id)) + if err != nil { + return types.GroupFolder{}, err + } + var r types.GroupFoldersResponse + res.JSON(&r) + if r.Ocs.Data.ID == 0 { + return types.GroupFolder{}, fmt.Errorf("%d is not a valid groupfolder's id", id) + } + return r.Ocs.Data.FormatGroupFolder(), nil +} + +func (c *Client) GroupFoldersCreate(name string) (id int, err error) { + // TODO: Validate Folder name + ro := &req.RequestOptions{ + Data: map[string]string{ + "mountpoint": name, + }, + } + res, err := c.baseRequest(http.MethodPost, routes.groupfolders, ro) + if err != nil { + return 0, err + } + var r types.GroupFoldersCreateResponse + res.JSON(&r) + id, _ = strconv.Atoi(r.Ocs.Data.ID) + return id, nil +} + +func (c *Client) GroupFoldersRename(groupID int, name string) error { + ro := &req.RequestOptions{ + Data: map[string]string{ + "mountpoint": name, + }, + } + // GroupFolders's response does not give any clues about success or failure + _, err := c.baseRequest(http.MethodPost, routes.groupfolders, ro, strconv.Itoa(groupID), "mountpoint") + if err != nil { + return err + } + return nil +} + +//TODO func (c *Client) GroupFoldersDelete(id int) error { +// // GroupFolders's response does not give any clues about success or failure +// return nil +//} + +func (c *Client) GroupFoldersAddGroup(folderID int, groupName string) error { + ro := &req.RequestOptions{ + Data: map[string]string{ + "group": groupName, + }, + } + // GroupFolders's response does not give any clues about success or failure + _, err := c.baseRequest(http.MethodPost, routes.groupfolders, ro, strconv.Itoa(folderID), "groups") + if err != nil { + return err + } + return nil +} + +func (c *Client) GroupFoldersRemoveGroup(folderID int, groupName string) error { + // GroupFolders's response does not give any clues about success or failure + _, err := c.baseRequest(http.MethodDelete, routes.groupfolders, nil, strconv.Itoa(folderID), "groups", groupName) + if err != nil { + return err + } + return nil +} + +func (c *Client) GroupFoldersSetGroupPermissions(folderID int, groupName string, permission types.SharePermission) error { + ro := &req.RequestOptions{ + Data: map[string]string{ + "permissions": strconv.Itoa(int(permission)), + }, + } + // GroupFolders's response does not give any clues about success or failure + _, err := c.baseRequest(http.MethodPost, routes.groupfolders, ro, strconv.Itoa(folderID), "groups", groupName) + if err != nil { + return err + } + return nil +} + +//GroupFoldersSetQuota set quota on the group folder. quota in bytes, use -3 for unlimited +func (c *Client) GroupFoldersSetQuota(folderID int, quota int) error { + ro := &req.RequestOptions{ + Data: map[string]string{ + "quota": strconv.Itoa(int(quota)), + }, + } + // GroupFolders's response does not give any clues about success or failure + _, err := c.baseRequest(http.MethodPost, routes.groupfolders, ro, strconv.Itoa(folderID), "quota") + if err != nil { + return err + } + return nil +} + +func formatBadIDAndGroups(g map[string]types.GroupFolderBadFormatIDAndGroups) map[int]types.GroupFolder { + var gfs = map[int]types.GroupFolder{} + for k := range g { + i, _ := strconv.Atoi(k) + d := g[k] + gfs[i] = d.FormatGroupFolder() + } + return gfs +} diff --git a/groups.go b/groups.go index 2ce3429..7c35e15 100644 --- a/groups.go +++ b/groups.go @@ -8,7 +8,7 @@ import ( //GroupList lists the Nextcloud groups func (c *Client) GroupList() ([]string, error) { - res, err := c.baseRequest(routes.groups, "", "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.groups, nil) if err != nil { return nil, err } @@ -19,7 +19,7 @@ func (c *Client) GroupList() ([]string, error) { //GroupUsers list the group's users func (c *Client) GroupUsers(name string) ([]string, error) { - res, err := c.baseRequest(routes.groups, name, "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.groups, nil, name) if err != nil { return nil, err } @@ -33,7 +33,7 @@ func (c *Client) GroupSearch(search string) ([]string, error) { ro := &req.RequestOptions{ Params: map[string]string{"search": search}, } - res, err := c.baseRequest(routes.groups, "", "", ro, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.groups, ro) if err != nil { return nil, err } @@ -49,17 +49,17 @@ func (c *Client) GroupCreate(name string) error { "groupid": name, }, } - return c.groupBaseRequest("", "", ro, http.MethodPost) + return c.groupBaseRequest(http.MethodPost, ro) } //GroupDelete deletes the group func (c *Client) GroupDelete(name string) error { - return c.groupBaseRequest(name, "", nil, http.MethodDelete) + return c.groupBaseRequest(http.MethodDelete, nil, name) } //GroupSubAdminList lists the group's subadmins func (c *Client) GroupSubAdminList(name string) ([]string, error) { - res, err := c.baseRequest(routes.groups, name, "subadmins", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.groups, nil, name, "subadmins") if err != nil { return nil, err } @@ -68,7 +68,7 @@ func (c *Client) GroupSubAdminList(name string) ([]string, error) { return r.Ocs.Data.Users, nil } -func (c *Client) groupBaseRequest(name string, route string, ro *req.RequestOptions, method string) error { - _, err := c.baseRequest(routes.groups, name, route, ro, method) +func (c *Client) groupBaseRequest(method string, ro *req.RequestOptions, subRoute ...string) error { + _, err := c.baseRequest(method, routes.groups, ro, subRoute...) return err } diff --git a/monitoring.go b/monitoring.go index c8ec145..be4b707 100644 --- a/monitoring.go +++ b/monitoring.go @@ -7,7 +7,7 @@ import ( //Monitoring return nextcloud monitoring statistics func (c *Client) Monitoring() (*types.Monitoring, error) { - res, err := c.baseRequest(routes.monitor, "", "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.monitor, nil) if err != nil { return nil, err } diff --git a/routes.go b/routes.go index b43e9f2..54bb0a2 100644 --- a/routes.go +++ b/routes.go @@ -10,6 +10,7 @@ type Routes struct { apps *url.URL monitor *url.URL shares *url.URL + groupfolders *url.URL } const badRequest = 998 @@ -23,5 +24,6 @@ var ( apps: &url.URL{Path: apiPath.Path + "/cloud/apps"}, monitor: &url.URL{Path: apiPath.Path + "/apps/serverinfo/api/v1/info"}, shares: &url.URL{Path: apiPath.Path + "/apps/files_sharing/api/v1/shares"}, + groupfolders: &url.URL{Path: "apps/groupfolders/folders"}, } ) diff --git a/shares.go b/shares.go index 09f61a5..59ce163 100644 --- a/shares.go +++ b/shares.go @@ -10,7 +10,7 @@ import ( ) func (c *Client) SharesList() ([]types.Share, error) { - res, err := c.baseRequest(routes.shares, "", "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.shares, nil) if err != nil { return nil, err } @@ -27,7 +27,7 @@ func (c *Client) Shares(path string, reshares bool, subfiles bool) ([]types.Shar "subfiles": strconv.FormatBool(subfiles), }, } - res, err := c.baseRequest(routes.shares, "", "", ro, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.shares, ro) if err != nil { return nil, err } @@ -37,7 +37,7 @@ func (c *Client) Shares(path string, reshares bool, subfiles bool) ([]types.Shar } func (c *Client) Share(shareID string) (types.Share, error) { - res, err := c.baseRequest(routes.shares, shareID, "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.shares, nil, shareID) if err != nil { return types.Share{}, err } @@ -68,7 +68,7 @@ func (c *Client) ShareCreate( "permissions": strconv.Itoa(int(permission)), }, } - res, err := c.baseRequest(routes.shares, "", "", ro, http.MethodPost) + res, err := c.baseRequest(http.MethodPost, routes.shares, ro) if err != nil { return types.Share{}, err } @@ -78,7 +78,7 @@ func (c *Client) ShareCreate( } func (c *Client) ShareDelete(shareID int) error { - _, err := c.baseRequest(routes.shares, strconv.Itoa(shareID), "", nil, http.MethodDelete) + _, err := c.baseRequest(http.MethodDelete, routes.shares, nil, strconv.Itoa(shareID)) return err } @@ -151,6 +151,6 @@ func (c *Client) baseShareUpdate(shareID string, key string, value string) error ro := &req.RequestOptions{ Data: map[string]string{key: value}, } - _, err := c.baseRequest(routes.shares, shareID, "", ro, http.MethodPut) + _, err := c.baseRequest(http.MethodPut, routes.shares, ro, shareID) return err } diff --git a/types/groupfolders.go b/types/groupfolders.go new file mode 100644 index 0000000..f43bab5 --- /dev/null +++ b/types/groupfolders.go @@ -0,0 +1,57 @@ +package types + +import "strconv" + +type GroupFolderBadFormatIDAndGroups struct { + ID string `json:"id"` + MountPoint string `json:"mount_point"` + Groups map[string]string `json:"groups"` + Quota string `json:"quota"` + Size int `json:"size"` +} + +type GroupFolderBadFormatGroups struct { + ID int `json:"id"` + MountPoint string `json:"mount_point"` + Groups map[string]string `json:"groups"` + Quota string `json:"quota"` + Size int `json:"size"` +} + +type GroupFolder struct { + ID int `json:"id"` + MountPoint string `json:"mount_point"` + Groups map[string]SharePermission `json:"groups"` + Quota int `json:"quota"` + Size int `json:"size"` +} + +func (gf *GroupFolderBadFormatGroups) FormatGroupFolder() GroupFolder { + g := GroupFolder{} + g.ID = gf.ID + g.MountPoint = gf.MountPoint + g.Groups = map[string]SharePermission{} + for k, v := range gf.Groups { + p, _ := strconv.Atoi(v) + g.Groups[k] = SharePermission(p) + } + q, _ := strconv.Atoi(gf.Quota) + g.Quota = q + g.Size = gf.Size + return g +} + +func (gf *GroupFolderBadFormatIDAndGroups) FormatGroupFolder() GroupFolder { + g := GroupFolder{} + g.ID, _ = strconv.Atoi(gf.ID) + g.MountPoint = gf.MountPoint + g.Groups = map[string]SharePermission{} + for k, v := range gf.Groups { + p, _ := strconv.Atoi(v) + g.Groups[k] = SharePermission(p) + } + q, _ := strconv.Atoi(gf.Quota) + g.Quota = q + g.Size = gf.Size + return g +} diff --git a/types/responses.go b/types/responses.go index d456d5a..970379e 100644 --- a/types/responses.go +++ b/types/responses.go @@ -108,3 +108,24 @@ type SharesResponse struct { Data Share `json:"data"` } `json:"ocs"` } + +type GroupFoldersListResponse struct { + Ocs struct { + Meta Meta `json:"meta"` + Data map[string]GroupFolderBadFormatIDAndGroups `json:"data"` + } `json:"ocs"` +} + +type GroupFoldersCreateResponse struct { + Ocs struct { + Meta Meta `json:"meta"` + Data GroupFolderBadFormatIDAndGroups `json:"data"` + } `json:"ocs"` +} + +type GroupFoldersResponse struct { + Ocs struct { + Meta Meta `json:"meta"` + Data GroupFolderBadFormatGroups `json:"data"` + } `json:"ocs"` +} diff --git a/users.go b/users.go index 36c3a43..097458e 100644 --- a/users.go +++ b/users.go @@ -14,7 +14,7 @@ import ( // UserList return the Nextcloud'user list func (c *Client) UserList() ([]string, error) { - res, err := c.baseRequest(routes.users, "", "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.users, nil) //res, err := c.session.Get(u.String(), nil) if err != nil { return nil, err @@ -29,7 +29,7 @@ func (c *Client) User(name string) (*types.User, error) { if name == "" { return nil, &types.APIError{Message: "name cannot be empty"} } - res, err := c.baseRequest(routes.users, name, "", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.users, nil, name) if err != nil { return nil, err } @@ -48,7 +48,7 @@ func (c *Client) UserSearch(search string) ([]string, error) { ro := &req.RequestOptions{ Params: map[string]string{"search": search}, } - res, err := c.baseRequest(routes.users, "", "", ro, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.users, ro) if err != nil { return nil, err } @@ -65,7 +65,7 @@ func (c *Client) UserCreate(username string, password string, user *types.User) "password": password, }, } - if err := c.userBaseRequest("", "", ro, http.MethodPost); err != nil { + if err := c.userBaseRequest(http.MethodPost, ro); err != nil { return err } if user == nil { @@ -76,7 +76,7 @@ func (c *Client) UserCreate(username string, password string, user *types.User) //UserDelete delete the user func (c *Client) UserDelete(name string) error { - return c.userBaseRequest(name, "", nil, http.MethodDelete) + return c.userBaseRequest(http.MethodDelete, nil, name) } //UserEnable enables the user @@ -84,7 +84,7 @@ func (c *Client) UserEnable(name string) error { ro := &req.RequestOptions{ Data: map[string]string{}, } - return c.userBaseRequest(name, "enable", ro, http.MethodPut) + return c.userBaseRequest(http.MethodPut, ro, name, "enable") } //UserDisable disables the user @@ -92,12 +92,12 @@ func (c *Client) UserDisable(name string) error { ro := &req.RequestOptions{ Data: map[string]string{}, } - return c.userBaseRequest(name, "disable", ro, http.MethodPut) + return c.userBaseRequest(http.MethodPut, ro, name, "disable") } //UserSendWelcomeEmail (re)send the welcome mail to the user (return an error if the user has not configured his email) func (c *Client) UserSendWelcomeEmail(name string) error { - return c.userBaseRequest(name, "welcome", nil, http.MethodPost) + return c.userBaseRequest(http.MethodPost, nil, name, "welcome") } //UserUpdate takes a *types.User struct to update the user's information @@ -168,7 +168,7 @@ func (c *Client) UserUpdateQuota(name string, quota int) error { //UserGroupList lists the user's groups func (c *Client) UserGroupList(name string) ([]string, error) { - res, err := c.baseRequest(routes.users, name, "groups", nil, http.MethodGet) + res, err := c.baseRequest(http.MethodGet, routes.users, nil, name, "groups") if err != nil { return nil, err } @@ -184,7 +184,7 @@ func (c *Client) UserGroupAdd(name string, group string) error { "groupid": group, }, } - return c.userBaseRequest(name, "groups", ro, http.MethodPost) + return c.userBaseRequest(http.MethodPost, ro, name, "groups") } //UserGroupRemove removes the user from the group @@ -194,7 +194,7 @@ func (c *Client) UserGroupRemove(name string, group string) error { "groupid": group, }, } - return c.userBaseRequest(name, "groups", ro, http.MethodDelete) + return c.userBaseRequest(http.MethodDelete, ro, name, "groups") } //UserGroupPromote promotes the user as group admin @@ -204,7 +204,7 @@ func (c *Client) UserGroupPromote(name string, group string) error { "groupid": group, }, } - return c.userBaseRequest(name, "subadmins", ro, http.MethodPost) + return c.userBaseRequest(http.MethodPost, ro, name, "subadmins") } //UserGroupDemote demotes the user @@ -214,7 +214,7 @@ func (c *Client) UserGroupDemote(name string, group string) error { "groupid": group, }, } - return c.userBaseRequest(name, "subadmins", ro, http.MethodDelete) + return c.userBaseRequest(http.MethodDelete, ro, name, "subadmins") } //UserGroupSubAdminList lists the groups where he is subadmin @@ -240,11 +240,11 @@ func (c *Client) userUpdateAttribute(name string, key string, value string) erro "value": value, }, } - return c.userBaseRequest(name, "", ro, http.MethodPut) + return c.userBaseRequest(http.MethodPut, ro, name) } -func (c *Client) userBaseRequest(name string, route string, ro *req.RequestOptions, method string) error { - _, err := c.baseRequest(routes.users, name, route, ro, method) +func (c *Client) userBaseRequest(method string, ro *req.RequestOptions, subRoutes ...string) error { + _, err := c.baseRequest(method, routes.users, ro, subRoutes...) return err } diff --git a/utils.go b/utils.go index 9d87df2..be81821 100644 --- a/utils.go +++ b/utils.go @@ -10,28 +10,29 @@ import ( "strings" ) -func (c *Client) baseRequest(route *url.URL, name string, subroute string, ro *req.RequestOptions, method string) (*req.Response, error) { +func (c *Client) baseRequest(method string, route *url.URL, ro *req.RequestOptions, subRoutes ...string) (*req.Response, error) { if !c.loggedIn() { return nil, errUnauthorized } u := c.baseURL.ResolveReference(route) - if name != "" { - u.Path = path.Join(u.Path, name) - } - if subroute != "" { - u.Path = path.Join(u.Path, subroute) + + for _, sr := range subRoutes { + if sr != "" { + u.Path = path.Join(u.Path, sr) + } } var ( res *req.Response err error ) - if method == http.MethodGet { + switch method { + case http.MethodGet: res, err = c.session.Get(u.String(), ro) - } else if method == http.MethodPost { + case http.MethodPost: res, err = c.session.Post(u.String(), ro) - } else if method == http.MethodPut { + case http.MethodPut: res, err = c.session.Put(u.String(), ro) - } else if method == http.MethodDelete { + case http.MethodDelete: res, err = c.session.Delete(u.String(), ro) } if err != nil { @@ -41,11 +42,11 @@ func (c *Client) baseRequest(route *url.URL, name string, subroute string, ro *r js := res.String() var r types.BaseResponse json.Unmarshal([]byte(js), &r) - if r.Ocs.Meta.Statuscode != 200 { - err := types.ErrorFromMeta(r.Ocs.Meta) - return nil, err + if r.Ocs.Meta.Statuscode == 200 || r.Ocs.Meta.Statuscode == 100 { + return res, nil } - return res, nil + err = types.ErrorFromMeta(r.Ocs.Meta) + return nil, err } func reformatJSON(json string) string { From 89cab88299c986e97695368484de9d85868f0317 Mon Sep 17 00:00:00 2001 From: Philippe-Adrien Nousse Date: Mon, 3 Sep 2018 12:21:42 +0200 Subject: [PATCH 3/4] Updated gitlab address in .gitlab-ci.yml and reset config.yml changes --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b5f6538..ef4a62e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,8 +7,8 @@ before_script: - go get -u golang.org/x/lint/golint - go get -u github.com/kardianos/govendor - mkdir -p /go/src/github.com/partitio -- cp -r $CI_PROJECT_DIR /go/src/github.com/partitio/gonextcloud -- cd /go/src/github.com/partitio/gonextcloud +- cp -r $CI_PROJECT_DIR /go/src/gitlab.adphi.fr/partitio/Nextcloud-Partitio/gonextcloud +- cd /go/src/gitlab.adphi.fr/partitio/Nextcloud-Partitio/gonextcloud - sed -i -e 's/$NEXTCLOUD_URL/'${NEXTCLOUD_URL//\//\\/}'/g' config.yml - sed -i -e 's/$NEXTCLOUD_PASSWORD/'${NEXTCLOUD_PASSWORD}'/g' config.yml - make dep From 6c8ca777aa9b9bef9580cfcafbcd0a15abdd259b Mon Sep 17 00:00:00 2001 From: Philippe-Adrien Nousse Date: Mon, 3 Sep 2018 12:23:20 +0200 Subject: [PATCH 4/4] Fix .gitlab-ci.yml --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ef4a62e..260f8e0 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,7 +6,7 @@ stages: before_script: - go get -u golang.org/x/lint/golint - go get -u github.com/kardianos/govendor -- mkdir -p /go/src/github.com/partitio +- mkdir -p /go/src/gitlab.adphi.fr/partitio/Nextcloud-Partitio/gonextcloud - cp -r $CI_PROJECT_DIR /go/src/gitlab.adphi.fr/partitio/Nextcloud-Partitio/gonextcloud - cd /go/src/gitlab.adphi.fr/partitio/Nextcloud-Partitio/gonextcloud - sed -i -e 's/$NEXTCLOUD_URL/'${NEXTCLOUD_URL//\//\\/}'/g' config.yml