mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
web client: use fetch to upload files
also add REST API to upload a single file as POST body
This commit is contained in:
parent
5b4ef0ee3b
commit
c153330ab8
19 changed files with 851 additions and 161 deletions
|
@ -288,7 +288,7 @@ func (t *BaseTransfer) updateTimes() {
|
|||
|
||||
func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
|
||||
// S3 uploads are atomic, if there is an error nothing is uploaded
|
||||
if t.File == nil && t.ErrTransfer != nil {
|
||||
if t.File == nil && t.ErrTransfer != nil && !t.Connection.User.HasBufferedSFTP(t.GetVirtualPath()) {
|
||||
return false
|
||||
}
|
||||
sizeDiff := fileSize - t.InitialSize
|
||||
|
|
|
@ -348,6 +348,15 @@ func (u *User) GetPermissionsForPath(p string) []string {
|
|||
return permissions
|
||||
}
|
||||
|
||||
// HasBufferedSFTP returns true if the user has a SFTP filesystem with buffering enabled
|
||||
func (u *User) HasBufferedSFTP(name string) bool {
|
||||
fs := u.GetFsConfigForPath(name)
|
||||
if fs.Provider == sdk.SFTPFilesystemProvider {
|
||||
return fs.SFTPConfig.BufferSize > 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
|
||||
sftpUser, err := UserExists(username)
|
||||
if err == nil {
|
||||
|
|
|
@ -10,11 +10,14 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
<textarea id="textarea_test" name="textarea_test" rows="6" cols="80">The text here will be sent to SFTPGo as blob</textarea>
|
||||
<textarea id="textarea_test" name="textarea_test" rows="10" cols="80">The text here will be sent to SFTPGo as blob</textarea>
|
||||
<br>
|
||||
<button onclick="saveBlob(false);">Save</button>
|
||||
<br>
|
||||
<button onclick="saveBlob(true);">Save binary file</button>
|
||||
<br><br>
|
||||
<b>Logs</b>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script type="text/javascript">
|
||||
|
@ -24,7 +27,7 @@
|
|||
// in real world usage set the origin when you call postMessage, we use `*` for testing purpose here
|
||||
$(document).ready(function () {
|
||||
if (window.opener == null || window.opener.closed) {
|
||||
console.log("window opener gone!");
|
||||
printLog("window opener gone!");
|
||||
return;
|
||||
}
|
||||
// notify SFTPGo that the page is ready to receive the file
|
||||
|
@ -33,30 +36,34 @@
|
|||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (window.opener == null || window.opener.closed) {
|
||||
console.log("window opener gone!");
|
||||
printLog("window opener gone!");
|
||||
return;
|
||||
}
|
||||
// you should check the origin before continuing
|
||||
console.log("new message: "+JSON.stringify(event.data));
|
||||
printLog("new message: "+JSON.stringify(event.data));
|
||||
switch (event.data.type){
|
||||
case 'readyResponse':
|
||||
// after sending the ready request SFTPGo will reply with this response
|
||||
// now you know the file name and the SFTPGo user
|
||||
fileName = event.data.file_name;
|
||||
sftpgoUser = event.data.user;
|
||||
console.log("ready response received, file name: " + fileName+" SFTPGo user: "+sftpgoUser);
|
||||
printLog("ready response received, file name: " + fileName+" SFTPGo user: "+sftpgoUser);
|
||||
// you can initialize your viewer/editor based on the file extension and request the blob
|
||||
window.opener.postMessage({type: 'sendBlob'}, "*");
|
||||
break;
|
||||
case 'blobDownloadStart':
|
||||
// SFTPGo may take a while to read the file, just before it starts reading it will send this message.
|
||||
// You can initialize a spinner if required for this file or simply ignore this message
|
||||
console.log("blob download start received, file name: " + fileName+" SFTPGo user: "+sftpgoUser);
|
||||
printLog("blob download start received, file name: " + fileName+" SFTPGo user: "+sftpgoUser);
|
||||
break;
|
||||
case 'blobDownloadError':
|
||||
// SFTPGo was unable to download the file and return it as blob
|
||||
printLog("blob download failed, file name: " + fileName+" SFTPGo user: "+sftpgoUser+" error message: "+event.data.message);
|
||||
break;
|
||||
case 'blob':
|
||||
// we received the file as blob
|
||||
var extension = fileName.slice((fileName.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
||||
console.log("blob received, file name: " + fileName+" extension: "+extension+" SFTPGo user: "+sftpgoUser);
|
||||
printLog("blob received, file name: " + fileName+" extension: "+extension+" SFTPGo user: "+sftpgoUser);
|
||||
if (extension == "txt"){
|
||||
event.data.file.text().then(function(text){
|
||||
$("#textarea_test").val(text);
|
||||
|
@ -65,25 +72,25 @@
|
|||
break;
|
||||
case 'blobSaveResult':
|
||||
// event.data.status is OK or KO, if KO message is not empty
|
||||
console.log("blob save status: "+event.data.status+", message: "+event.data.message);
|
||||
printLog("blob save status: "+event.data.status+", message: "+event.data.message);
|
||||
if (event.data.status == "OK"){
|
||||
console.log("blob saved, I'm useless now, close me");
|
||||
printLog("blob saved, I'm useless now, close me");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
console.log("Unsupported message: " + JSON.stringify(event.data));
|
||||
printLog("Unsupported message: " + JSON.stringify(event.data));
|
||||
}
|
||||
});
|
||||
|
||||
function saveBlob(binary){
|
||||
if (window.opener == null || window.opener.closed) {
|
||||
console.log("window opener gone!");
|
||||
printLog("window opener gone!");
|
||||
return;
|
||||
}
|
||||
// if we have modified the file we can send it back to SFTPGo as a blob for saving
|
||||
console.log("save blob, binary? "+binary);
|
||||
printLog("save blob, binary? "+binary);
|
||||
if (binary){
|
||||
// we download and save the SFTPGo logo
|
||||
// we download and save the SFTPGo logo. In a real application check errors
|
||||
fetch('https://raw.githubusercontent.com/drakkan/sftpgo/main/docs/howto/img/logo.png')
|
||||
.then(response => response.blob())
|
||||
.then(function(responseBlob){
|
||||
|
@ -101,5 +108,11 @@
|
|||
},"*");
|
||||
}
|
||||
}
|
||||
|
||||
function printLog(message){
|
||||
console.log(message);
|
||||
var logger = document.getElementById('log');
|
||||
logger.innerHTML += message + '<br>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
10
go.mod
10
go.mod
|
@ -7,7 +7,7 @@ require (
|
|||
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
||||
github.com/aws/aws-sdk-go v1.42.19
|
||||
github.com/aws/aws-sdk-go v1.42.20
|
||||
github.com/cockroachdb/cockroach-go/v2 v2.2.4
|
||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||
github.com/fclairamb/ftpserverlib v0.16.0
|
||||
|
@ -25,7 +25,7 @@ require (
|
|||
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||
github.com/klauspost/compress v1.13.6
|
||||
github.com/lestrrat-go/jwx v1.2.12
|
||||
github.com/lestrrat-go/jwx v1.2.13
|
||||
github.com/lib/pq v1.10.4
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||
github.com/mattn/go-sqlite3 v1.14.9
|
||||
|
@ -53,7 +53,7 @@ require (
|
|||
gocloud.dev v0.24.0
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
|
||||
golang.org/x/sys v0.0.0-20211204120058-94396e421777
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
google.golang.org/api v0.61.0
|
||||
google.golang.org/grpc v1.42.0
|
||||
|
@ -62,7 +62,7 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.98.0 // indirect
|
||||
cloud.google.com/go v0.99.0 // indirect
|
||||
github.com/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1 // indirect
|
||||
|
@ -129,7 +129,7 @@ require (
|
|||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
|
|
23
go.sum
23
go.sum
|
@ -33,8 +33,8 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
|
|||
cloud.google.com/go v0.94.0/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
||||
cloud.google.com/go v0.98.0 h1:w6LozQJyDDEyhf64Uusu1LCcnLt0I1VMLiJC2kV+eXk=
|
||||
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
|
||||
cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
|
||||
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
@ -136,8 +136,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo
|
|||
github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.38.68/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.42.19 h1:L/aM1QwsqVia9qIqexTHwYN+lgLYuOtf11VDgz0YIyw=
|
||||
github.com/aws/aws-sdk-go v1.42.19/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go v1.42.20 h1:nQkkmTWK5N2Ao1iVzoOx1HTIxwbSWErxyZ1eiwLJWc4=
|
||||
github.com/aws/aws-sdk-go v1.42.20/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
|
||||
github.com/aws/aws-sdk-go-v2 v1.7.0/go.mod h1:tb9wi5s61kTDA5qCkcDbt3KRVV74GGslQkl/DRdX/P4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
|
@ -301,7 +301,6 @@ github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22
|
|||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/goccy/go-json v0.7.6/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.7.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
|
||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
|
@ -566,8 +565,8 @@ github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++
|
|||
github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
|
||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
||||
github.com/lestrrat-go/jwx v1.2.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
||||
github.com/lestrrat-go/jwx v1.2.12 h1:XG2R0T2w5YFBy7Clfionp6cDdDPuo3yU5xFJg5/r91o=
|
||||
github.com/lestrrat-go/jwx v1.2.12/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw=
|
||||
github.com/lestrrat-go/jwx v1.2.13 h1:GxuOPfAz4+nzL98WaKxBxEEZ9b7qmyDetMGfBm9yVvE=
|
||||
github.com/lestrrat-go/jwx v1.2.13/go.mod h1:3Q3Re8TaOcVTdpx4Tvz++OWmryDklihTDqrrwQiyS2A=
|
||||
github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
|
@ -991,8 +990,8 @@ golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211204120058-94396e421777 h1:QAkhGVjOxMa+n4mlsAWeAU+BMZmimQAaNiMu+iUi94E=
|
||||
golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
|
@ -1197,9 +1196,9 @@ google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEc
|
|||
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 h1:fU3FNfL/oBU2D5DvGqiuyVqqn40DdxvaTFHq7aivA3k=
|
||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf h1:PSEM+IQFb9xdsj2CGhfqUTfsZvF8DScCVP1QZb2IiTQ=
|
||||
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||
google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
|
@ -169,6 +170,85 @@ func getUserFile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func setFileDirMetadata(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
|
||||
metadata := make(map[string]int64)
|
||||
err := render.DecodeJSON(r.Body, &metadata)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
mTime, ok := metadata["modification_time"]
|
||||
if !ok || !r.URL.Query().Has("path") {
|
||||
sendAPIResponse(w, r, errors.New("please set a modification_time and a path"), "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
connection, err := getUserConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
common.Connections.Add(connection)
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
name := util.CleanPath(r.URL.Query().Get("path"))
|
||||
attrs := common.StatAttributes{
|
||||
Flags: common.StatAttrTimes,
|
||||
Atime: util.GetTimeFromMsecSinceEpoch(mTime),
|
||||
Mtime: util.GetTimeFromMsecSinceEpoch(mTime),
|
||||
}
|
||||
err = connection.SetStat(name, &attrs)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to set metadata for path %#v", name), getMappedStatusCode(err))
|
||||
return
|
||||
}
|
||||
sendAPIResponse(w, r, nil, "OK", http.StatusOK)
|
||||
}
|
||||
|
||||
func uploadUserFile(w http.ResponseWriter, r *http.Request) {
|
||||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
|
||||
if !r.URL.Query().Has("path") {
|
||||
sendAPIResponse(w, r, errors.New("please set a file path"), "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
connection, err := getUserConnection(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
common.Connections.Add(connection)
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
|
||||
filePath := util.CleanPath(r.URL.Query().Get("path"))
|
||||
doUploadFile(w, r, connection, filePath) //nolint:errcheck
|
||||
}
|
||||
|
||||
func doUploadFile(w http.ResponseWriter, r *http.Request, connection *Connection, filePath string) error {
|
||||
writer, err := connection.getFileWriter(filePath)
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Unable to write file %#v", filePath), getMappedStatusCode(err))
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(writer, r.Body)
|
||||
if err != nil {
|
||||
writer.Close() //nolint:errcheck
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Error saving file %#v", filePath), getMappedStatusCode(err))
|
||||
return err
|
||||
}
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
sendAPIResponse(w, r, err, fmt.Sprintf("Error closing file %#v", filePath), getMappedStatusCode(err))
|
||||
return err
|
||||
}
|
||||
setModificationTimeFromHeader(r, connection, filePath)
|
||||
sendAPIResponse(w, r, nil, "Upload completed", http.StatusCreated)
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadUserFiles(w http.ResponseWriter, r *http.Request) {
|
||||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
|
@ -468,3 +548,23 @@ func doChangeUserPassword(r *http.Request, currentPassword, newPassword, confirm
|
|||
|
||||
return dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
}
|
||||
|
||||
func setModificationTimeFromHeader(r *http.Request, c *Connection, filePath string) {
|
||||
mTimeString := r.Header.Get(mTimeHeader)
|
||||
if mTimeString != "" {
|
||||
// we don't return an error here if we fail to set the modification time
|
||||
mTime, err := strconv.ParseInt(mTimeString, 10, 64)
|
||||
if err == nil {
|
||||
attrs := common.StatAttributes{
|
||||
Flags: common.StatAttrTimes,
|
||||
Atime: util.GetTimeFromMsecSinceEpoch(mTime),
|
||||
Mtime: util.GetTimeFromMsecSinceEpoch(mTime),
|
||||
}
|
||||
err = c.SetStat(filePath, &attrs)
|
||||
c.Log(logger.LevelDebug, "requested modification time %v for file %#v, error: %v",
|
||||
attrs.Mtime, filePath, err)
|
||||
} else {
|
||||
c.Log(logger.LevelInfo, "invalid modification time header was ignored: %v", mTimeString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/go-chi/render"
|
||||
"github.com/rs/xid"
|
||||
|
@ -176,7 +177,30 @@ func downloadFromShare(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func uploadToShare(w http.ResponseWriter, r *http.Request) {
|
||||
func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
|
||||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
name := getURLParam(r, "name")
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
filePath := path.Join(share.Paths[0], name)
|
||||
if path.Dir(filePath) != share.Paths[0] {
|
||||
sendAPIResponse(w, r, err, "Uploading outside the share is not allowed", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dataprovider.UpdateShareLastUse(&share, 1) //nolint:errcheck
|
||||
|
||||
common.Connections.Add(connection)
|
||||
defer common.Connections.Remove(connection.GetID())
|
||||
if err := doUploadFile(w, r, connection, filePath); err != nil {
|
||||
dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
|
||||
}
|
||||
}
|
||||
|
||||
func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
|
||||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
|
|
|
@ -62,6 +62,8 @@ const (
|
|||
userFilePath = "/api/v2/user/file"
|
||||
userFilesPath = "/api/v2/user/files"
|
||||
userStreamZipPath = "/api/v2/user/streamzip"
|
||||
userUploadFilePath = "/api/v2/user/files/upload"
|
||||
userFilesDirsMetadataPath = "/api/v2/user/files/metadata"
|
||||
apiKeysPath = "/api/v2/apikeys"
|
||||
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
|
||||
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
|
||||
|
@ -120,6 +122,7 @@ const (
|
|||
webClientTwoFactorPathDefault = "/web/client/twofactor"
|
||||
webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
|
||||
webClientFilesPathDefault = "/web/client/files"
|
||||
webClientFilePathDefault = "/web/client/file"
|
||||
webClientSharesPathDefault = "/web/client/shares"
|
||||
webClientSharePathDefault = "/web/client/share"
|
||||
webClientEditFilePathDefault = "/web/client/editfile"
|
||||
|
@ -147,6 +150,7 @@ const (
|
|||
maxMultipartMem = 10485760 // 10 MB
|
||||
osWindows = "windows"
|
||||
otpHeaderCode = "X-SFTPGO-OTP"
|
||||
mTimeHeader = "X-SFTPGO-MTIME"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -195,6 +199,7 @@ var (
|
|||
webClientTwoFactorPath string
|
||||
webClientTwoFactorRecoveryPath string
|
||||
webClientFilesPath string
|
||||
webClientFilePath string
|
||||
webClientSharesPath string
|
||||
webClientSharePath string
|
||||
webClientEditFilePath string
|
||||
|
@ -578,6 +583,7 @@ func updateWebClientURLs(baseURL string) {
|
|||
webClientTwoFactorPath = path.Join(baseURL, webClientTwoFactorPathDefault)
|
||||
webClientTwoFactorRecoveryPath = path.Join(baseURL, webClientTwoFactorRecoveryPathDefault)
|
||||
webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
|
||||
webClientFilePath = path.Join(baseURL, webClientFilePathDefault)
|
||||
webClientSharesPath = path.Join(baseURL, webClientSharesPathDefault)
|
||||
webClientPubSharesPath = path.Join(baseURL, webClientPubSharesPathDefault)
|
||||
webClientSharePath = path.Join(baseURL, webClientSharePathDefault)
|
||||
|
|
|
@ -91,6 +91,8 @@ const (
|
|||
userDirsPath = "/api/v2/user/dirs"
|
||||
userFilesPath = "/api/v2/user/files"
|
||||
userStreamZipPath = "/api/v2/user/streamzip"
|
||||
userUploadFilePath = "/api/v2/user/files/upload"
|
||||
userFilesDirsMetadataPath = "/api/v2/user/files/metadata"
|
||||
apiKeysPath = "/api/v2/apikeys"
|
||||
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
|
||||
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
|
||||
|
@ -8677,6 +8679,14 @@ func TestPreUploadHook(t *testing.T) {
|
|||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=filepre",
|
||||
bytes.NewBuffer([]byte("single upload content")))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
|
||||
err = os.WriteFile(preActionPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
_, err = reader.Seek(0, io.SeekStart)
|
||||
|
@ -8688,6 +8698,14 @@ func TestPreUploadHook(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=filepre",
|
||||
bytes.NewBuffer([]byte("single upload content")))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
|
@ -8929,6 +8947,94 @@ func TestShareUsage(t *testing.T) {
|
|||
executeRequest(req)
|
||||
}
|
||||
|
||||
func TestShareUploadSingle(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
token, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
share := dataprovider.Share{
|
||||
Name: "test share",
|
||||
Scope: dataprovider.ShareScopeWrite,
|
||||
Paths: []string{"/"},
|
||||
Password: defaultPassword,
|
||||
MaxTokens: 0,
|
||||
}
|
||||
asJSON, err := json.Marshal(share)
|
||||
assert.NoError(t, err)
|
||||
req, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, token)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
objectID := rr.Header().Get("X-Object-ID")
|
||||
assert.NotEmpty(t, objectID)
|
||||
|
||||
content := []byte("shared file content")
|
||||
modTime := time.Now().Add(-12 * time.Hour)
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "file.txt"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
req.Header.Set("X-SFTPGO-MTIME", strconv.FormatInt(util.GetTimeAsMsSinceEpoch(modTime), 10))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
info, err := os.Stat(filepath.Join(user.GetHomeDir(), "file.txt"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))
|
||||
}
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webClientPubSharesPath, objectID, "file.txt"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
info, err = os.Stat(filepath.Join(user.GetHomeDir(), "file.txt"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000))
|
||||
}
|
||||
// we don't allow to create the file in subdirectories
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "%2Fdir%2Ffile1.txt"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir", "file.dat"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "%2F"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
err = os.MkdirAll(filepath.Join(user.GetHomeDir(), "dir"), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "dir"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusInternalServerError, rr)
|
||||
assert.Contains(t, rr.Body.String(), "operation unsupported")
|
||||
|
||||
share, err = dataprovider.ShareExists(objectID, user.Username)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, share.UsedTokens)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(sharesPath, objectID, "file1.txt"), bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
req.SetBasicAuth(defaultUsername, defaultPassword)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestShareUncompressed(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -9985,6 +10091,119 @@ func TestWebDirsAPI(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
}
|
||||
|
||||
func TestWebUploadSingleFile(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, defaultPassword)
|
||||
assert.NoError(t, err)
|
||||
|
||||
content := []byte("test content")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, userUploadFilePath, bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "please set a file path")
|
||||
|
||||
modTime := time.Now().Add(-24 * time.Hour)
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=file.txt", bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
req.Header.Set("X-SFTPGO-MTIME", strconv.FormatInt(util.GetTimeAsMsSinceEpoch(modTime), 10))
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
|
||||
info, err := os.Stat(filepath.Join(user.GetHomeDir(), "file.txt"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))
|
||||
}
|
||||
// invalid modification time will be ignored
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=file.txt", bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
req.Header.Set("X-SFTPGO-MTIME", "123abc")
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
info, err = os.Stat(filepath.Join(user.GetHomeDir(), "file.txt"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(time.Now()), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(3000))
|
||||
}
|
||||
|
||||
metadataReq := make(map[string]int64)
|
||||
metadataReq["modification_time"] = util.GetTimeAsMsSinceEpoch(modTime)
|
||||
asJSON, err := json.Marshal(metadataReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+"?path=file.txt", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
info, err = os.Stat(filepath.Join(user.GetHomeDir(), "file.txt"))
|
||||
if assert.NoError(t, err) {
|
||||
assert.InDelta(t, util.GetTimeAsMsSinceEpoch(modTime), util.GetTimeAsMsSinceEpoch(info.ModTime()), float64(1000))
|
||||
}
|
||||
// missing file
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+"?path=file2.txt", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to set metadata for path")
|
||||
// invalid JSON
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+"?path=file.txt", bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
// missing mandatory parameter
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath, bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "please set a modification_time and a path")
|
||||
|
||||
metadataReq = make(map[string]int64)
|
||||
asJSON, err = json.Marshal(metadataReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+"?path=file.txt", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||
assert.Contains(t, rr.Body.String(), "please set a modification_time and a path")
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=%2Fdir%2Ffile.txt", bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to write file")
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=file.txt", bytes.NewBuffer(content))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user")
|
||||
|
||||
metadataReq["modification_time"] = util.GetTimeAsMsSinceEpoch(modTime)
|
||||
asJSON, err = json.Marshal(metadataReq)
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPatch, userFilesDirsMetadataPath+"?path=file.txt", bytes.NewBuffer(asJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Unable to retrieve your user")
|
||||
}
|
||||
|
||||
func TestWebFilesAPI(t *testing.T) {
|
||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -10281,7 +10500,7 @@ func TestWebUploadErrors(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusNotFound, rr)
|
||||
|
||||
if runtime.GOOS != osWindows {
|
||||
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.zip", reader)
|
||||
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.zip", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
|
@ -10301,6 +10520,13 @@ func TestWebUploadErrors(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Error closing file")
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=file.zip", bytes.NewBuffer(nil))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusForbidden, rr)
|
||||
assert.Contains(t, rr.Body.String(), "Error closing file")
|
||||
|
||||
err = os.Chmod(user.GetHomeDir(), os.ModePerm)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
@ -10577,6 +10803,30 @@ func TestWebUploadSFTP(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
||||
assert.Contains(t, rr.Body.String(), "denying write due to space limit")
|
||||
assert.Contains(t, rr.Body.String(), "Unable to write file")
|
||||
|
||||
// delete the file
|
||||
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
user, _, err = httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), user.UsedQuotaSize)
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, userUploadFilePath+"?path=file.txt",
|
||||
bytes.NewBuffer([]byte("test upload single file content")))
|
||||
assert.NoError(t, err)
|
||||
req.Header.Add("Content-Type", writer.FormDataContentType())
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
||||
assert.Contains(t, rr.Body.String(), "denying write due to space limit")
|
||||
assert.Contains(t, rr.Body.String(), "Error saving file")
|
||||
|
||||
// delete the file
|
||||
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -10593,6 +10843,19 @@ func TestWebUploadSFTP(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
||||
assert.Contains(t, rr.Body.String(), "denying write due to space limit")
|
||||
assert.Contains(t, rr.Body.String(), "Error saving file")
|
||||
|
||||
// delete the file
|
||||
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.txt", nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, webAPIToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
user, _, err = httpdtest.GetUserByUsername(sftpUser.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, user.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(0), user.UsedQuotaSize)
|
||||
|
||||
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
|
|
@ -81,6 +81,7 @@ func (s *httpdServer) listenAndServe() error {
|
|||
config := &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
|
@ -1004,7 +1005,8 @@ func (s *httpdServer) initializeRouter() {
|
|||
|
||||
// share API exposed to external users
|
||||
s.router.Get(sharesPath+"/{id}", downloadFromShare)
|
||||
s.router.Post(sharesPath+"/{id}", uploadToShare)
|
||||
s.router.Post(sharesPath+"/{id}", uploadFilesToShare)
|
||||
s.router.Post(sharesPath+"/{id}/{name}", uploadFileToShare)
|
||||
|
||||
s.router.Get(tokenPath, s.getToken)
|
||||
s.router.Post(adminPath+"/{username}/forgot-password", forgotAdminPassword)
|
||||
|
@ -1155,6 +1157,8 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Get(userSharesPath+"/{id}", getShareByID)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Put(userSharesPath+"/{id}", updateShare)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Delete(userSharesPath+"/{id}", deleteShare)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Post(userUploadFilePath, uploadUserFile)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled)).Patch(userFilesDirsMetadataPath, setFileDirMetadata)
|
||||
})
|
||||
|
||||
if s.renderOpenAPI {
|
||||
|
@ -1215,7 +1219,8 @@ func (s *httpdServer) initializeRouter() {
|
|||
Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
|
||||
// share API exposed to external users
|
||||
s.router.Get(webClientPubSharesPath+"/{id}", downloadFromShare)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}", uploadToShare)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}", uploadFilesToShare)
|
||||
s.router.Post(webClientPubSharesPath+"/{id}/{name}", uploadFileToShare)
|
||||
|
||||
s.router.Group(func(router chi.Router) {
|
||||
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie))
|
||||
|
@ -1224,8 +1229,9 @@ func (s *httpdServer) initializeRouter() {
|
|||
router.Get(webClientLogoutPath, handleWebClientLogout)
|
||||
router.With(s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
|
||||
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
|
||||
router.With(s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Post(webClientFilesPath, uploadUserFiles)
|
||||
Post(webClientFilePath, uploadUserFile)
|
||||
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
|
||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||
Patch(webClientFilesPath, renameUserFile)
|
||||
|
|
|
@ -109,6 +109,7 @@ type viewPDFPage struct {
|
|||
type editFilePage struct {
|
||||
baseClientPage
|
||||
CurrentDir string
|
||||
FileURL string
|
||||
Path string
|
||||
Name string
|
||||
ReadOnly bool
|
||||
|
@ -121,6 +122,7 @@ type filesPage struct {
|
|||
DirsURL string
|
||||
DownloadURL string
|
||||
ViewPDFURL string
|
||||
FileURL string
|
||||
CanAddFiles bool
|
||||
CanCreateDirs bool
|
||||
CanRename bool
|
||||
|
@ -412,6 +414,7 @@ func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileDa
|
|||
Path: fileName,
|
||||
Name: path.Base(fileName),
|
||||
CurrentDir: path.Dir(fileName),
|
||||
FileURL: webClientFilePath,
|
||||
ReadOnly: readOnly,
|
||||
Data: fileData,
|
||||
}
|
||||
|
@ -447,6 +450,7 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
|
|||
DownloadURL: webClientDownloadZipPath,
|
||||
ViewPDFURL: webClientViewPDFPath,
|
||||
DirsURL: webClientDirsPath,
|
||||
FileURL: webClientFilePath,
|
||||
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
||||
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
||||
CanRename: user.CanRenameFromWeb(dirName, dirName),
|
||||
|
@ -616,6 +620,8 @@ func (s *httpdServer) handleClientGetDirContents(w http.ResponseWriter, r *http.
|
|||
for idx := range s.binding.WebClientIntegrations {
|
||||
if util.IsStringInSlice(extension, s.binding.WebClientIntegrations[idx].FileExtensions) {
|
||||
res["ext_url"] = s.binding.WebClientIntegrations[idx].URL
|
||||
res["ext_link"] = fmt.Sprintf("%v?path=%v&_=%v", webClientFilePath,
|
||||
url.QueryEscape(path.Join(name, info.Name())), time.Now().UTC().Unix())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -100,7 +100,7 @@ paths:
|
|||
tags:
|
||||
- public shares
|
||||
summary: Upload one or more files to the shared path
|
||||
description: The share must be defined with the read scope and the associated user must have the upload permission
|
||||
description: The share must be defined with the write scope and the associated user must have the upload permission
|
||||
operationId: upload_to_share
|
||||
requestBody:
|
||||
content:
|
||||
|
@ -137,6 +137,77 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/shares/{id}/{fileName}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
description: the share id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: fileName
|
||||
in: path
|
||||
description: the name of the new file. It must be path encoded. Sub directories are not accepted
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
- name: X-SFTPGO-MTIME
|
||||
in: header
|
||||
schema:
|
||||
type: integer
|
||||
description: File modification time as unix timestamp in milliseconds
|
||||
post:
|
||||
security:
|
||||
- BasicAuth: []
|
||||
tags:
|
||||
- public shares
|
||||
summary: Upload a single file to the shared path
|
||||
description: The share must be defined with the write scope and the associated user must have the upload/overwrite permissions
|
||||
operationId: upload_single_to_share
|
||||
requestBody:
|
||||
content:
|
||||
application/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
text/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
image/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
audio/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
video/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
required: true
|
||||
responses:
|
||||
'201':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'413':
|
||||
$ref: '#/components/responses/RequestEntityTooLarge'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/token:
|
||||
get:
|
||||
security:
|
||||
|
@ -3701,6 +3772,114 @@ paths:
|
|||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/files/upload:
|
||||
post:
|
||||
tags:
|
||||
- user APIs
|
||||
summary: Upload a single file
|
||||
description: 'Upload a single file for the logged in user to an existing directory. This API does not use multipart/form-data and so no temporary files are created server side but only a single file can be uploaded as POST body'
|
||||
operationId: create_user_file
|
||||
parameters:
|
||||
- in: query
|
||||
name: path
|
||||
description: Full file path. It must be path encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt". The parent directory must exist. If a file with the same name already exists, it will be overwritten
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
- in: header
|
||||
name: X-SFTPGO-MTIME
|
||||
schema:
|
||||
type: integer
|
||||
description: File modification time as unix timestamp in milliseconds
|
||||
requestBody:
|
||||
content:
|
||||
application/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
text/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
image/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
audio/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
video/*:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
required: true
|
||||
responses:
|
||||
'201':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'413':
|
||||
$ref: '#/components/responses/RequestEntityTooLarge'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/files/metadata:
|
||||
patch:
|
||||
tags:
|
||||
- user APIs
|
||||
summary: Set metadata for a file/directory
|
||||
description: 'Set supported metadata attributes for the specified file or directory'
|
||||
operationId: setprops_user_file
|
||||
parameters:
|
||||
- in: query
|
||||
name: path
|
||||
description: Full file/directory path. It must be URL encoded, for example the path "my dir/àdir/file.txt" must be sent as "my%20dir%2F%C3%A0dir%2Ffile.txt"
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
modification_time:
|
||||
type: integer
|
||||
description: File modification time as unix timestamp in milliseconds
|
||||
required: true
|
||||
responses:
|
||||
'200':
|
||||
description: successful operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ApiResponse'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequest'
|
||||
'401':
|
||||
$ref: '#/components/responses/Unauthorized'
|
||||
'403':
|
||||
$ref: '#/components/responses/Forbidden'
|
||||
'413':
|
||||
$ref: '#/components/responses/RequestEntityTooLarge'
|
||||
'500':
|
||||
$ref: '#/components/responses/InternalServerError'
|
||||
default:
|
||||
$ref: '#/components/responses/DefaultResponse'
|
||||
/user/streamzip:
|
||||
post:
|
||||
tags:
|
||||
|
|
|
@ -105,6 +105,7 @@ func (c Conf) Initialize(configDir string) error {
|
|||
config := &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
CipherSuites: util.GetTLSCiphersFromNames(c.TLSCipherSuites),
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
|
|
|
@ -155,7 +155,7 @@ function deleteAction() {
|
|||
var selectedRows = table.rows({ selected: true }).count();
|
||||
if (selectedRows == 1){
|
||||
var folderName = table.row({ selected: true }).data()[0];
|
||||
var path = '{{.FolderTemplateURL}}' + "?from=" + fixedEncodeURIComponent(folderName);
|
||||
var path = '{{.FolderTemplateURL}}' + "?from=" + encodeURIComponent(folderName);
|
||||
window.location.href = path;
|
||||
} else {
|
||||
window.location.href = '{{.FolderTemplateURL}}';
|
||||
|
|
|
@ -159,7 +159,7 @@
|
|||
titleAttr: "Clone",
|
||||
action: function (e, dt, node, config) {
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserURL}}' + "?clone-from=" + fixedEncodeURIComponent(username);
|
||||
var path = '{{.UserURL}}' + "?clone-from=" + encodeURIComponent(username);
|
||||
window.location.href = path;
|
||||
},
|
||||
enabled: false
|
||||
|
@ -172,7 +172,7 @@
|
|||
var selectedRows = table.rows({ selected: true }).count();
|
||||
if (selectedRows == 1){
|
||||
var username = dt.row({ selected: true }).data()[1];
|
||||
var path = '{{.UserTemplateURL}}' + "?from=" + fixedEncodeURIComponent(username);
|
||||
var path = '{{.UserTemplateURL}}' + "?from=" + encodeURIComponent(username);
|
||||
window.location.href = path;
|
||||
} else {
|
||||
window.location.href = '{{.UserTemplateURL}}';
|
||||
|
|
|
@ -134,43 +134,52 @@
|
|||
{{if not .ReadOnly}}
|
||||
function saveFile() {
|
||||
$('#idSave').addClass("disabled");
|
||||
cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||
var path = '{{.FilesURL}}?path={{.CurrentDir}}';
|
||||
var data = new FormData();
|
||||
var blob = new Blob([cm.getValue()]);
|
||||
data.append("filenames", new File([blob], "{{.Name}}"));
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
||||
timeout: 15000,
|
||||
success: function (result) {
|
||||
window.location.href = '{{.FilesURL}}?path={{.CurrentDir}}';
|
||||
async function uploadFile() {
|
||||
var errorMessage = "Error saving file";
|
||||
let response;
|
||||
try {
|
||||
var uploadPath = '{{.FileURL}}?path='+encodeURIComponent('{{.CurrentDir}}/{{.Name}}');
|
||||
var cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||
var blob = new Blob([cm.getValue()]);
|
||||
response = await fetch(uploadPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
credentials: 'same-origin',
|
||||
redirect: 'error',
|
||||
body: blob
|
||||
});
|
||||
if (response.status == 201){
|
||||
window.location.href = '{{.FilesURL}}?path='+encodeURIComponent('{{.CurrentDir}}');
|
||||
} else {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = await response.json();
|
||||
} catch(e){
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
if (jsonResponse.message) {
|
||||
errorMessage = jsonResponse.message;
|
||||
}
|
||||
if (jsonResponse.error) {
|
||||
errorMessage += ": " + jsonResponse.error;
|
||||
}
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
} catch (e){
|
||||
throw Error(errorMessage+": " +e.message);
|
||||
}
|
||||
}
|
||||
|
||||
uploadFile().catch(function(error){
|
||||
$('#idSave').removeClass("disabled");
|
||||
var txt = "Error saving file";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
}
|
||||
if (json.error) {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorTxt').text(error.message);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
}
|
||||
{{end}}
|
||||
|
|
|
@ -210,7 +210,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
function notifySave(status, message){
|
||||
function notifyBlobDownloadError(message) {
|
||||
if (childReference == null || childReference.closed) {
|
||||
console.log("external window null or closed, cannot notify download error");
|
||||
return;
|
||||
}
|
||||
|
||||
childReference.postMessage({
|
||||
type: 'blobDownloadError',
|
||||
message: message
|
||||
}, childProps.get('url'));
|
||||
}
|
||||
|
||||
function notifySave(status, message) {
|
||||
if (childReference == null || childReference.closed) {
|
||||
console.log("external window null or closed, cannot notify save");
|
||||
return;
|
||||
|
@ -243,70 +255,118 @@
|
|||
}, childProps.get('url'));
|
||||
break;
|
||||
case 'sendBlob':
|
||||
// we have to download the blob, this can require some time so
|
||||
// we have to download the blob, this could require some time so
|
||||
// we first send a blobDownloadStart message so the child can
|
||||
// show a spinner or something similar
|
||||
var errorMessage = "Error downloading file";
|
||||
childReference.postMessage({
|
||||
type: 'blobDownloadStart'
|
||||
}, childProps.get('url'));
|
||||
// download the file and send as blob to the child window
|
||||
fetch(childProps.get('link'))
|
||||
.then(response => response.blob())
|
||||
.then(function(responseBlob){
|
||||
// download the file and send it as blob to the child window
|
||||
async function downloadFileAsBlob(){
|
||||
var errorMessage = "Error downloading file";
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(childProps.get('link'),{
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
redirect: 'error'
|
||||
});
|
||||
} catch (e){
|
||||
throw Error(errorMessage+": " +e.message);
|
||||
}
|
||||
if (response.status == 200){
|
||||
let responseBlob;
|
||||
try {
|
||||
responseBlob = await response.blob();
|
||||
} catch (e){
|
||||
throw Error(errorMessage+" as blob: " +e.message);
|
||||
}
|
||||
let fileBlob = new File([responseBlob], childProps.get('file_name'), {type: responseBlob.type, lastModified: ""});
|
||||
childReference.postMessage({
|
||||
type: 'blob',
|
||||
file: fileBlob
|
||||
}, childProps.get('url'));
|
||||
} else {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = await response.json();
|
||||
} catch(e){
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
if (jsonResponse.message) {
|
||||
errorMessage = jsonResponse.message;
|
||||
}
|
||||
if (jsonResponse.error) {
|
||||
errorMessage += ": " + jsonResponse.error;
|
||||
}
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
downloadFileAsBlob().catch(function(error){
|
||||
notifyBlobDownloadError(error.message);
|
||||
$('#errorTxt').text(error.message);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 5000);
|
||||
});
|
||||
break;
|
||||
case 'saveBlob':
|
||||
// get the blob from the message and save it
|
||||
var path = '{{.FilesURL}}?path={{.CurrentDir}}';
|
||||
spinnerDone = false;
|
||||
var file = new File([event.data.file], childProps.get('file_name'));
|
||||
var data = new FormData();
|
||||
data.append('filenames', file);
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
||||
timeout: 0,
|
||||
beforeSend: function () {
|
||||
$('#spinnerModal').modal('show');
|
||||
|
||||
async function saveBlob() {
|
||||
var errorMessage = "Error saving external file";
|
||||
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+childProps.get('file_name'));
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(uploadPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
success: function (result) {
|
||||
credentials: 'same-origin',
|
||||
redirect: 'error',
|
||||
body: event.data.file
|
||||
});
|
||||
} catch (e){
|
||||
throw Error(errorMessage+": " +e.message);
|
||||
}
|
||||
if (response.status == 201){
|
||||
$('#spinnerModal').modal('hide');
|
||||
notifySave("OK", "");
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 2000);
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
} else {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = await response.json();
|
||||
} catch(e){
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
if (jsonResponse.message) {
|
||||
errorMessage = jsonResponse.message;
|
||||
}
|
||||
if (jsonResponse.error) {
|
||||
errorMessage += ": " + jsonResponse.error;
|
||||
}
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
saveBlob().catch(function(error){
|
||||
$('#spinnerModal').modal('hide');
|
||||
var txt = "Error saving external file";
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
}
|
||||
if (json.error) {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
notifySave("KO", txt);
|
||||
$('#errorTxt').text(txt);
|
||||
notifySave("KO", error.message);
|
||||
$('#errorTxt').text(error.message);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 5000);
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
|
@ -465,7 +525,7 @@
|
|||
} else {
|
||||
path = '{{.FilesURL}}';
|
||||
}
|
||||
path+='?path={{.CurrentDir}}'+fixedEncodeURIComponent("/"+itemName);
|
||||
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName);
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
|
@ -526,7 +586,7 @@
|
|||
event.preventDefault();
|
||||
$('#createDirModal').modal('hide');
|
||||
var dirName = replaceSlash($("#directory_name").val());
|
||||
var path = '{{.DirsURL}}?path={{.CurrentDir}}' + fixedEncodeURIComponent("/"+dirName);
|
||||
var path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent("/"+dirName);
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
|
@ -562,7 +622,6 @@
|
|||
event.preventDefault();
|
||||
keepAlive();
|
||||
var keepAliveTimer = setInterval(keepAlive, 300000);
|
||||
var path = '{{.FilesURL}}?path={{.CurrentDir}}';
|
||||
|
||||
var files = $("#files_name")[0].files;
|
||||
var has_errors = false;
|
||||
|
@ -584,48 +643,57 @@
|
|||
}
|
||||
return;
|
||||
}
|
||||
//console.log("upload file, index: "+index);
|
||||
var data = new FormData();
|
||||
data.append('filenames', files[index]);
|
||||
|
||||
$.ajax({
|
||||
url: path,
|
||||
type: 'POST',
|
||||
data: data,
|
||||
processData: false,
|
||||
contentType: false,
|
||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
||||
timeout: 0,
|
||||
success: function (result) {
|
||||
async function saveFile() {
|
||||
//console.log("save file, index: "+index);
|
||||
var errorMessage = "Error uploading files";
|
||||
let response;
|
||||
try {
|
||||
var f = files[index];
|
||||
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
|
||||
response = await fetch(uploadPath, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-SFTPGO-MTIME': f.lastModified,
|
||||
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
redirect: 'error',
|
||||
body: f
|
||||
});
|
||||
} catch (e){
|
||||
throw Error(errorMessage+": " +e.message);
|
||||
}
|
||||
if (response.status == 201){
|
||||
index++;
|
||||
success++;
|
||||
uploadFile();
|
||||
},
|
||||
error: function ($xhr, textStatus, errorThrown) {
|
||||
} else {
|
||||
let jsonResponse;
|
||||
try {
|
||||
jsonResponse = await response.json();
|
||||
} catch(e){
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
if (jsonResponse.message) {
|
||||
errorMessage = jsonResponse.message;
|
||||
}
|
||||
if (jsonResponse.error) {
|
||||
errorMessage += ": " + jsonResponse.error;
|
||||
}
|
||||
throw Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
saveFile().catch(function(error){
|
||||
index++;
|
||||
has_errors = true;
|
||||
var txt = "Error uploading files";
|
||||
if (success > 0){
|
||||
txt = "Not all files have been uploaded, please reload the page";
|
||||
}
|
||||
if ($xhr) {
|
||||
var json = $xhr.responseJSON;
|
||||
if (json) {
|
||||
if (json.message) {
|
||||
txt = json.message;
|
||||
}
|
||||
if (json.error) {
|
||||
txt += ": " + json.error;
|
||||
}
|
||||
}
|
||||
}
|
||||
$('#errorTxt').text(txt);
|
||||
$('#errorTxt').text(error.message);
|
||||
$('#errorMsg').show();
|
||||
setTimeout(function () {
|
||||
$('#errorMsg').hide();
|
||||
}, 10000);
|
||||
uploadFile();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -646,7 +714,7 @@
|
|||
} else {
|
||||
path = '{{.FilesURL}}';
|
||||
}
|
||||
path+='?path={{.CurrentDir}}'+fixedEncodeURIComponent("/"+itemName)+'&target={{.CurrentDir}}'+fixedEncodeURIComponent("/"+targetName);
|
||||
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName)+'&target={{.CurrentDir}}'+encodeURIComponent("/"+targetName);
|
||||
$('renameModal').modal('hide');
|
||||
$.ajax({
|
||||
url: path,
|
||||
|
@ -698,7 +766,7 @@
|
|||
for (i = 0; i < selected.length; i++) {
|
||||
filesArray.push(getNameFromMeta(selected[i]));
|
||||
}
|
||||
var files = fixedEncodeURIComponent(JSON.stringify(filesArray));
|
||||
var files = encodeURIComponent(JSON.stringify(filesArray));
|
||||
var downloadURL = '{{.DownloadURL}}';
|
||||
var currentDir = '{{.CurrentDir}}';
|
||||
var ts = new Date().getTime().toString();
|
||||
|
@ -763,7 +831,7 @@
|
|||
for (i = 0; i < selected.length; i++) {
|
||||
filesArray.push(getNameFromMeta(selected[i]));
|
||||
}
|
||||
var files = fixedEncodeURIComponent(JSON.stringify(filesArray));
|
||||
var files = encodeURIComponent(JSON.stringify(filesArray));
|
||||
var shareURL = '{{.ShareURL}}';
|
||||
var currentDir = '{{.CurrentDir}}';
|
||||
var ts = new Date().getTime().toString();
|
||||
|
@ -808,6 +876,7 @@
|
|||
if (!data.sftpgo_dir || data.sftpgo_dir != '{{.CurrentDir}}'){
|
||||
data.start = 0;
|
||||
data.search.search = "";
|
||||
data.checkboxes = [];
|
||||
}
|
||||
},
|
||||
"columns": [
|
||||
|
@ -874,7 +943,7 @@
|
|||
{{if .HasIntegrations}}
|
||||
if (type === 'display') {
|
||||
if (data){
|
||||
return `<a href="#" onclick="openExternalURL('${data}', '${row["url"]}', '${row["name"]}');"><i class="fas fa-external-link-alt"></i></a>`;
|
||||
return `<a href="#" onclick="openExternalURL('${data}', '${row["ext_link"]}', '${row["name"]}');"><i class="fas fa-external-link-alt"></i></a>`;
|
||||
}
|
||||
}
|
||||
{{end}}
|
||||
|
|
|
@ -99,6 +99,9 @@
|
|||
<p>You can upload one or more files to the shared directory by sending a multipart/form-data request to this <a id="writeLink" href="#" target="_blank">link</a>. The form field name for the file(s) is <b><code>filenames</code></b>.</p>
|
||||
<p>For example:</p>
|
||||
<p><code>curl -F filenames=@file1.txt -F filenames=@file2.txt "share link"</code></p>
|
||||
<p>You can upload files one by one by adding the path encoded file name to the share <a id="writeLinkSingle" href="#" target="_blank">link</a> and sending the file as POST body. The optional <b><code>X-SFTPGO-MTIME</code></b> header allows to set the file modification time as milliseconds since epoch.</p>
|
||||
<p>For example:</p>
|
||||
<p><code>curl --data-binary @file.txt -H "Content-Type: application/octet-stream" -H "X-SFTPGO-MTIME: 1638882991234" "share link/file.txt"</code></p>
|
||||
</div>
|
||||
<div id="expiredShare">
|
||||
This share is no longer accessible because it has expired
|
||||
|
@ -222,6 +225,8 @@
|
|||
$('#readShare').hide();
|
||||
$('#writeLink').attr("href", shareURL);
|
||||
$('#writeLink').attr("title", shareURL);
|
||||
$('#writeLinkSingle').attr("href", shareURL);
|
||||
$('#writeLinkSingle').attr("title", shareURL);
|
||||
}
|
||||
}
|
||||
$('#linkModal').modal('show');
|
||||
|
|
|
@ -59,6 +59,7 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
|
|||
httpServer.TLSConfig = &tls.Config{
|
||||
GetCertificate: certMgr.GetCertificateFunc(),
|
||||
MinVersion: tls.VersionTLS12,
|
||||
NextProtos: []string{"http/1.1", "h2"},
|
||||
CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue