Skip to content
This repository has been archived by the owner on Jan 31, 2024. It is now read-only.

Commit

Permalink
Merge pull request #38 from gmgigi96/download-endpoint
Browse files Browse the repository at this point in the history
Download endpoint for public links
  • Loading branch information
glpatcern authored and gmgigi96 committed Nov 22, 2022
2 parents 175d104 + 2f2cdc8 commit 1ff7887
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 2 deletions.
224 changes: 224 additions & 0 deletions internal/http/services/owncloud/ocdav/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
package ocdav

import (
"context"
"io"
"net/http"
"path"
"strings"

gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
"github.com/cs3org/reva/internal/http/services/archiver/manager"
"github.com/cs3org/reva/pkg/appctx"
ctxpkg "github.com/cs3org/reva/pkg/ctx"
"github.com/cs3org/reva/pkg/errtypes"
"github.com/cs3org/reva/pkg/rhttp"
"github.com/cs3org/reva/pkg/storage/utils/downloader"
"github.com/cs3org/reva/pkg/storage/utils/walker"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"google.golang.org/grpc/metadata"
)

// index.php/s/jIKrtrkXCIXwg1y/download?path=%2FHugo&files=Intrinsico
func (s *svc) handleLegacyPublicLinkDownload(w http.ResponseWriter, r *http.Request) {
token := strings.TrimPrefix(r.URL.Path, "/")
files := getFilesFromRequest(r)
s.downloadFiles(r.Context(), w, token, files)
}

func getFilesFromRequest(r *http.Request) []string {
q := r.URL.Query()
dir := q.Get("path")
files := []string{}

if q.Get("files") != "" {
files = append(files, path.Join(dir, q.Get("files")))
} else {
for _, f := range q["files[]"] {
files = append(files, path.Join(dir, f))
}
}
return files
}

func (s *svc) authenticate(ctx context.Context, token string) (context.Context, error) {
// TODO (gdelmont): support password protected public links
c, err := s.getClient()
if err != nil {
return nil, err
}
res, err := c.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "publicshares",
ClientId: token,
ClientSecret: "password|",
})
if err != nil {
return nil, err
}

if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound(token)
}
return nil, errors.New(res.Status.Message)
}

ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, res.Token)
ctx = ctxpkg.ContextSetToken(ctx, res.Token)

return ctx, nil
}

func (s *svc) handleHttpError(w http.ResponseWriter, err error, log *zerolog.Logger) {
log.Error().Err(err).Msg("ocdav: got error")
switch err.(type) {
case errtypes.NotFound:
http.Error(w, "Resource not found", http.StatusNotFound)
case manager.ErrMaxSize, manager.ErrMaxFileCount:
http.Error(w, err.Error(), http.StatusRequestEntityTooLarge)
default:
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

func (s *svc) downloadFiles(ctx context.Context, w http.ResponseWriter, token string, files []string) {
log := appctx.GetLogger(ctx)
ctx, err := s.authenticate(ctx, token)
if err != nil {
s.handleHttpError(w, err, log)
return
}
isSingleFileShare, res, err := s.isSingleFileShare(ctx, token, files)
if err != nil {
s.handleHttpError(w, err, log)
return
}
if isSingleFileShare {
s.downloadFile(ctx, w, res)
} else {
s.downloadArchive(ctx, w, token, files)
}
}

func (s *svc) isSingleFileShare(ctx context.Context, token string, files []string) (bool, *provider.ResourceInfo, error) {
switch len(files) {
case 0:
return s.resourceIsFileInPublicLink(ctx, token, "")
case 1:
return s.resourceIsFileInPublicLink(ctx, token, files[0])
default:
// FIXME (gdelmont): even if the list contains more than one file
// these (or part of them), could not exist
// in this case, filtering the existing ones, we could
// end up having 0 or 1 files
return false, nil, nil
}
}

func (s *svc) resourceIsFileInPublicLink(ctx context.Context, token, file string) (bool, *provider.ResourceInfo, error) {
res, err := s.getResourceFromPublicLinkToken(ctx, token, file)
if err != nil {
return false, nil, err
}
return res.Type == provider.ResourceType_RESOURCE_TYPE_FILE, res, nil
}

func (s *svc) getResourceFromPublicLinkToken(ctx context.Context, token, file string) (*provider.ResourceInfo, error) {
c, err := s.getClient()
if err != nil {
return nil, err
}
res, err := c.GetPublicShareByToken(ctx, &link.GetPublicShareByTokenRequest{
Token: token,
})
if err != nil {
return nil, err
}

if res.Status.Code != rpc.Code_CODE_OK {
if res.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound(token)
}
return nil, errtypes.InternalError(res.Status.Message)
}

statRes, err := c.Stat(ctx, &provider.StatRequest{Ref: &provider.Reference{ResourceId: res.Share.ResourceId, Path: file}})
if err != nil {
return nil, err
}

if statRes.Status.Code != rpc.Code_CODE_OK {
if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND {
return nil, errtypes.NotFound(token)
}
return nil, errtypes.InternalError(statRes.Status.Message)
}
return statRes.Info, nil
}

func (s *svc) downloadFile(ctx context.Context, w http.ResponseWriter, res *provider.ResourceInfo) {
log := appctx.GetLogger(ctx)
c, err := s.getClient()
if err != nil {
s.handleHttpError(w, err, log)
return
}
d := downloader.NewDownloader(c)
r, err := d.Download(ctx, res.Path)
if err != nil {
s.handleHttpError(w, err, log)
return
}
defer r.Close()

w.WriteHeader(http.StatusOK)

_, err = io.Copy(w, r)
if err != nil {
s.handleHttpError(w, err, log)
return
}
}

func getPublicLinkResources(rootFolder, token string, files []string) []string {
r := make([]string, 0, len(files))
for _, f := range files {
r = append(r, path.Join(rootFolder, token, f))
}
if len(r) == 0 {
r = []string{path.Join(rootFolder, token)}
}
return r
}

func (s *svc) downloadArchive(ctx context.Context, w http.ResponseWriter, token string, files []string) {
log := appctx.GetLogger(ctx)
resources := getPublicLinkResources(s.c.PublicLinkDownload.PublicFolder, token, files)

gtw, err := s.getClient()
if err != nil {
s.handleHttpError(w, err, log)
return
}

downloader := downloader.NewDownloader(gtw, rhttp.Context(ctx))
walker := walker.NewWalker(gtw)

archiver, err := manager.NewArchiver(resources, walker, downloader, manager.Config{
MaxNumFiles: s.c.PublicLinkDownload.MaxNumFiles,
MaxSize: s.c.PublicLinkDownload.MaxSize,
})
if err != nil {
s.handleHttpError(w, err, log)
return
}

if err := archiver.CreateTar(ctx, w); err != nil {
s.handleHttpError(w, err, log)
return
}
}
19 changes: 17 additions & 2 deletions internal/http/services/owncloud/ocdav/ocdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ func init() {
global.Register("ocdav", New)
}

type ConfigPublicLinkDownload struct {
MaxNumFiles int64 `mapstructure:"max_num_files"`
MaxSize int64 `mapstructure:"max_size"`
PublicFolder string `mapstructure:"public_folder"`
}

// Config holds the config options that need to be passed down to all ocdav handlers
type Config struct {
Prefix string `mapstructure:"prefix"`
Expand All @@ -104,6 +110,7 @@ type Config struct {
PublicURL string `mapstructure:"public_url"`
FavoriteStorageDriver string `mapstructure:"favorite_storage_driver"`
FavoriteStorageDrivers map[string]map[string]interface{} `mapstructure:"favorite_storage_drivers"`
PublicLinkDownload *ConfigPublicLinkDownload `mapstructure:"publiclink_download"`
}

func (c *Config) init() {
Expand Down Expand Up @@ -173,7 +180,7 @@ func (s *svc) Close() error {
}

func (s *svc) Unprotected() []string {
return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/"}
return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/s/"}
}

func (s *svc) Handler() http.Handler {
Expand All @@ -198,6 +205,14 @@ func (s *svc) Handler() http.Handler {
head, r.URL.Path = router.ShiftPath(r.URL.Path)
log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing")
switch head {
case "s":
if strings.HasSuffix(r.URL.Path, "/download") {
r.URL.Path = strings.TrimSuffix(r.URL.Path, "/download")
s.handleLegacyPublicLinkDownload(w, r)
return
}
http.Error(w, "Not Yet Implemented", http.StatusNotImplemented)
return
case "status.php":
s.doStatus(w, r)
return
Expand All @@ -218,7 +233,7 @@ func (s *svc) Handler() http.Handler {
if head == "s" {
token := r.URL.Path
rURL := s.c.PublicURL + path.Join(head, token)

r.URL.Path = "/" // reset old path for redirection
http.Redirect(w, r, rURL, http.StatusMovedPermanently)
return
}
Expand Down

0 comments on commit 1ff7887

Please sign in to comment.