Skip to content

Commit

Permalink
Merge pull request #49 from gary-kim/enh/noid/file-uploads
Browse files Browse the repository at this point in the history
  • Loading branch information
gary-kim committed Jun 4, 2021
2 parents 8e1fc0a + 415d050 commit d36c18c
Show file tree
Hide file tree
Showing 37 changed files with 9,410 additions and 2 deletions.
8 changes: 7 additions & 1 deletion constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,14 @@
package constants

const (
// OCSBaseEndpoint is the base OCS api endpoint for Nextcloud
OCSBaseEndpoint = "/ocs/v2.php/"

// BaseEndpoint is the api endpoint for Nextcloud Talk
BaseEndpoint = "/ocs/v2.php/apps/spreed/api/v1/"
BaseEndpoint = OCSBaseEndpoint + "apps/spreed/api/v1/"

// FilesSharingEndpoint is the api endpoint for the Nextcloud files_sharing app
FilesSharingEndpoint = OCSBaseEndpoint + "apps/files_sharing/api/v1/"
)

// RemoteDavEndpoint returns the endpoint for the Dav API for Nextcloud
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ go 1.13
require (
github.com/monaco-io/request v1.0.5
github.com/stretchr/testify v1.7.0
github.com/studio-b12/gowebdav v0.0.0-20210427212133-86f8378cf140
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/studio-b12/gowebdav v0.0.0-20210427212133-86f8378cf140 h1:JCSn/2k3AQ0aJGs5Yx2xv6qrW0CAULc1E+xtSxeeQ/E=
github.com/studio-b12/gowebdav v0.0.0-20210427212133-86f8378cf140/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
16 changes: 16 additions & 0 deletions ocs/share.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package ocs

const (
// ShareTypeRoom is OC.share.SHARE_TYPE_ROOM
ShareTypeRoom = "10"
)

// ShareReturn is the response for a file share request
type ShareReturn struct {
OCS struct {
ocs
Data struct {
URL string `json:"url"`
} `json:"data"`
} `json:"ocs"`
}
36 changes: 36 additions & 0 deletions room/share.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package room

import (
"encoding/json"
"net/http"

"github.com/monaco-io/request"

"gomod.garykim.dev/nc-talk/constants"
"gomod.garykim.dev/nc-talk/ocs"
)

// ShareFile shares the file at the given path with the talk room
func (t *TalkRoom) ShareFile(path string) (string, error) {
req := t.User.RequestClient(request.Client{
URL: constants.FilesSharingEndpoint + "shares",
Params: map[string]string{
"shareType": ocs.ShareTypeRoom,
"path": path,
"shareWith": t.Token,
},
})
resp, err := req.Do()
if err != nil {
return "", err
}
if resp.StatusCode() != http.StatusOK {
return "", ErrUnexpectedReturnCode
}
data := &ocs.ShareReturn{}
err = json.Unmarshal(resp.Data, data)
if err != nil {
return "", err
}
return data.OCS.Data.URL, nil
}
125 changes: 125 additions & 0 deletions user/uploads.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package user

import (
"errors"
"os"
"path"
"regexp"
"strconv"
"strings"

"gomod.garykim.dev/nc-talk/constants"

"github.com/studio-b12/gowebdav"
)

var (
duplicateSuffixRegex = regexp.MustCompile(` \((\d+)\)$`)

// ErrIsNotDirectory is returned if a path expected to be a directory is not a directory
ErrIsNotDirectory = errors.New("is not a directory")
)

// UploadFile uploads the given data to the talk attachments
// directory and returns the filepath it was uploaded at.
//
// If the directory does not exist, the function will create the directory.
//
// Provide only the name of the file, not the full path. If a full path is provided,
// only the basename will be used. Use user.TalkUser.UploadFileAtPath for uploading
// to a specific directory.
func (t *TalkUser) UploadFile(data *[]byte, name string) (finalPath string, err error) {
capabilities, err := t.Capabilities()
if err != nil {
return
}
finalPath = path.Clean(capabilities.AttachmentsFolder + "/" + path.Base(name))
finalPath, err = t.uploadFilePath(finalPath)
if err != nil {
return
}
err = t.UploadFileAtPath(data, finalPath)
return
}

// UploadFileAtPath uploads the given data at the given path.
func (t *TalkUser) UploadFileAtPath(data *[]byte, givenPath string) error {
c := t.getWebdavClient()
err := c.Write(givenPath, *data, 0644)
if err != nil {
if ferr, ok := err.(*os.PathError); !ok || strings.HasPrefix(ferr.Err.Error(), "404") {
err = c.MkdirAll(path.Dir(givenPath), 0644)
if err != nil {
return err
}
err = c.Write(givenPath, *data, 0644)
}
}
return err
}

// uploadFilePath provides a unique path to upload a file with the given path.
func (t *TalkUser) uploadFilePath(name string) (finalPath string, err error) {
c := t.getWebdavClient()

capabilities, err := t.Capabilities()
if err != nil {
return
}

statInfo, err := c.Stat(capabilities.AttachmentsFolder)
if err != nil {
// The error may be because it does not exist
if strings.HasPrefix(err.(*os.PathError).Err.Error(), "404 Not Found - PROPFIND") {
// Directory does not exist. Return the given path directly.
return name, nil
}
return
}
if !statInfo.IsDir() {
return "", ErrIsNotDirectory
}

files, err := c.ReadDir(capabilities.AttachmentsFolder)
if err != nil {
return
}

filename := path.Base(name)
if !includesFileName(files, filename) {
return filename, nil
}

extension := path.Ext(filename)
basename := strings.TrimSuffix(filename, extension)

suffix := duplicateSuffixRegex.Find([]byte(basename))
nameWithoutSuffix := strings.TrimSuffix(basename, string(suffix))

// Loop until a unique path is found
for i := 2; true; i++ {
uniqueName := nameWithoutSuffix + " (" + strconv.Itoa(i) + ")" + extension
if !includesFileName(files, uniqueName) {
finalPath = path.Dir(name) + "/" + uniqueName
return
}
}
return "", errors.New("should never reach")
}

func includesFileName(filelist []os.FileInfo, name string) bool {
for _, v := range filelist {
if v.Name() == name {
return true
}
}
return false
}

func (t *TalkUser) getWebdavClient() *gowebdav.Client {
if t.webdavclient == nil {
url := t.NextcloudURL + constants.RemoteDavEndpoint(t.User, "files")
t.webdavclient = gowebdav.NewClient(url, t.User, t.Pass)
}
return t.webdavclient
}
106 changes: 106 additions & 0 deletions user/uploads_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package user

import (
"fmt"
"net"
"net/http"
"strconv"
"testing"

"github.com/monaco-io/request"

"github.com/stretchr/testify/assert"
"golang.org/x/net/webdav"
)

func startTestServer() (int, error) {
wd := &webdav.Handler{
Prefix: "/remote.php/dav/files/testuser/",
FileSystem: webdav.NewMemFS(),
LockSystem: webdav.NewMemLS(),
}

listener, err := net.Listen("tcp", ":0")
if err != nil {
return 0, err
}
port := listener.Addr().(*net.TCPAddr).Port
go func() {
fmt.Println("Serving on port: " + strconv.Itoa(port))
err := http.Serve(listener, wd)
if err != nil {
panic(err)
}
}()
return port, err
}

func TestFileUploads(t *testing.T) {
port, err := startTestServer()
assert.NoError(t, err, "Starting server")

user, err := NewUser("http://localhost:"+strconv.Itoa(port), "testuser", "password", nil)
assert.NoError(t, err, "Creating a new user")

user.capabilities = &Capabilities{
AttachmentsFolder: "/Talk",
AttachmentsAllowed: true,
}

someBytes := &[]byte{21, 65, 56, 57, 2, 95, 100, 85}

filepath, err := user.UploadFile(someBytes, "testfile.txt")
assert.NoError(t, err, "creating testfile.txt")
assert.Equal(t, "/Talk/testfile.txt", filepath, "ensuring file is in the expected location")

c := user.RequestClient(request.Client{
URL: "/remote.php/dav/files/testuser/Talk/testfile.txt",
})
resp, err := c.Do()
assert.NoError(t, err, "downloading file to check it is identical")
assert.Equal(t, someBytes, &resp.Data, "checking that the data is the same after being downloaded")

filepath, err = user.UploadFile(someBytes, "testfile.txt")
assert.NoError(t, err, "creating a second testfile.txt")
assert.Equal(t, "/Talk/testfile (2).txt", filepath, "ensuring file is in the expected location")

c = user.RequestClient(request.Client{
URL: "/remote.php/dav/files/testuser/Talk/testfile (2).txt",
})
resp, err = c.Do()
assert.NoError(t, err, "downloading file to check it is identical")
assert.Equal(t, someBytes, &resp.Data, "checking that the data is the same after being downloaded")

filepath, err = user.UploadFile(someBytes, "testfile.txt")
assert.NoError(t, err, "creating a third testfile.txt")
assert.Equal(t, "/Talk/testfile (3).txt", filepath, "ensuring file is in the expected location")

c = user.RequestClient(request.Client{
URL: "/remote.php/dav/files/testuser/Talk/testfile (3).txt",
})
resp, err = c.Do()
assert.NoError(t, err, "downloading file to check it is identical")
assert.Equal(t, someBytes, &resp.Data, "checking that the data is the same after being downloaded")

filepath, err = user.UploadFile(someBytes, "testfile (3).txt")
assert.NoError(t, err, "creating a file with an already existing increment: testfile (2).txt")
assert.Equal(t, "/Talk/testfile (4).txt", filepath, "ensuring file is in the expected location")

err = user.UploadFileAtPath(someBytes, "/asdf/asdf/testing.txt")
assert.NoError(t, err, "attempting upload with UploadFileAtPath")

c = user.RequestClient(request.Client{
URL: "/remote.php/dav/files/testuser/asdf/asdf/testing.txt",
})
resp, err = c.Do()
assert.NoError(t, err, "downloading file to check it is identical")
assert.Equal(t, someBytes, &resp.Data, "checking that the data is the same after being downloaded")

// Sanity check
c = user.RequestClient(request.Client{
URL: "/remote.php/dav/files/testuser/asdf/asdf/testing bla.txt",
})
resp, err = c.Do()
assert.NoError(t, err, "downloading file to check it is identical")
assert.Equal(t, http.StatusNotFound, resp.StatusCode(), "making sure a file that does not exist is not found")
}
6 changes: 5 additions & 1 deletion user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import (
"crypto/tls"
"encoding/json"
"errors"
"net/http"
"strings"

"github.com/monaco-io/request"
"github.com/studio-b12/gowebdav"

"gomod.garykim.dev/nc-talk/constants"
"gomod.garykim.dev/nc-talk/ocs"
Expand All @@ -46,7 +48,9 @@ type TalkUser struct {
Pass string
NextcloudURL string
Config *TalkUserConfig

capabilities *Capabilities
webdavclient *gowebdav.Client
}

// TalkUserConfig is configuration options for TalkUsers
Expand Down Expand Up @@ -312,7 +316,7 @@ func (t *TalkUser) DownloadFile(path string) (data *[]byte, err error) {
if err != nil {
return
}
if res.StatusCode() != 200 {
if res.StatusCode() != http.StatusOK {
err = ErrCannotDownloadFile
return
}
Expand Down
19 changes: 19 additions & 0 deletions vendor/github.com/studio-b12/gowebdav/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions vendor/github.com/studio-b12/gowebdav/.travis.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d36c18c

Please sign in to comment.