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 {
|
func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
|
||||||
// S3 uploads are atomic, if there is an error nothing is uploaded
|
// 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
|
return false
|
||||||
}
|
}
|
||||||
sizeDiff := fileSize - t.InitialSize
|
sizeDiff := fileSize - t.InitialSize
|
||||||
|
|
|
@ -348,6 +348,15 @@ func (u *User) GetPermissionsForPath(p string) []string {
|
||||||
return permissions
|
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) {
|
func (u *User) getForbiddenSFTPSelfUsers(username string) ([]string, error) {
|
||||||
sftpUser, err := UserExists(username)
|
sftpUser, err := UserExists(username)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|
|
@ -10,11 +10,14 @@
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
<br>
|
||||||
<button onclick="saveBlob(false);">Save</button>
|
<button onclick="saveBlob(false);">Save</button>
|
||||||
<br>
|
<br>
|
||||||
<button onclick="saveBlob(true);">Save binary file</button>
|
<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 src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||||
<script type="text/javascript">
|
<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
|
// in real world usage set the origin when you call postMessage, we use `*` for testing purpose here
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
if (window.opener == null || window.opener.closed) {
|
if (window.opener == null || window.opener.closed) {
|
||||||
console.log("window opener gone!");
|
printLog("window opener gone!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// notify SFTPGo that the page is ready to receive the file
|
// notify SFTPGo that the page is ready to receive the file
|
||||||
|
@ -33,30 +36,34 @@
|
||||||
|
|
||||||
window.addEventListener('message', (event) => {
|
window.addEventListener('message', (event) => {
|
||||||
if (window.opener == null || window.opener.closed) {
|
if (window.opener == null || window.opener.closed) {
|
||||||
console.log("window opener gone!");
|
printLog("window opener gone!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// you should check the origin before continuing
|
// 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){
|
switch (event.data.type){
|
||||||
case 'readyResponse':
|
case 'readyResponse':
|
||||||
// after sending the ready request SFTPGo will reply with this response
|
// after sending the ready request SFTPGo will reply with this response
|
||||||
// now you know the file name and the SFTPGo user
|
// now you know the file name and the SFTPGo user
|
||||||
fileName = event.data.file_name;
|
fileName = event.data.file_name;
|
||||||
sftpgoUser = event.data.user;
|
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
|
// you can initialize your viewer/editor based on the file extension and request the blob
|
||||||
window.opener.postMessage({type: 'sendBlob'}, "*");
|
window.opener.postMessage({type: 'sendBlob'}, "*");
|
||||||
break;
|
break;
|
||||||
case 'blobDownloadStart':
|
case 'blobDownloadStart':
|
||||||
// SFTPGo may take a while to read the file, just before it starts reading it will send this message.
|
// 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
|
// 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;
|
break;
|
||||||
case 'blob':
|
case 'blob':
|
||||||
// we received the file as blob
|
// we received the file as blob
|
||||||
var extension = fileName.slice((fileName.lastIndexOf(".") - 1 >>> 0) + 2).toLowerCase();
|
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"){
|
if (extension == "txt"){
|
||||||
event.data.file.text().then(function(text){
|
event.data.file.text().then(function(text){
|
||||||
$("#textarea_test").val(text);
|
$("#textarea_test").val(text);
|
||||||
|
@ -65,25 +72,25 @@
|
||||||
break;
|
break;
|
||||||
case 'blobSaveResult':
|
case 'blobSaveResult':
|
||||||
// event.data.status is OK or KO, if KO message is not empty
|
// 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"){
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
console.log("Unsupported message: " + JSON.stringify(event.data));
|
printLog("Unsupported message: " + JSON.stringify(event.data));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function saveBlob(binary){
|
function saveBlob(binary){
|
||||||
if (window.opener == null || window.opener.closed) {
|
if (window.opener == null || window.opener.closed) {
|
||||||
console.log("window opener gone!");
|
printLog("window opener gone!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// if we have modified the file we can send it back to SFTPGo as a blob for saving
|
// 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){
|
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')
|
fetch('https://raw.githubusercontent.com/drakkan/sftpgo/main/docs/howto/img/logo.png')
|
||||||
.then(response => response.blob())
|
.then(response => response.blob())
|
||||||
.then(function(responseBlob){
|
.then(function(responseBlob){
|
||||||
|
@ -101,5 +108,11 @@
|
||||||
},"*");
|
},"*");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function printLog(message){
|
||||||
|
console.log(message);
|
||||||
|
var logger = document.getElementById('log');
|
||||||
|
logger.innerHTML += message + '<br>';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -7,7 +7,7 @@ require (
|
||||||
github.com/Azure/azure-storage-blob-go v0.14.0
|
github.com/Azure/azure-storage-blob-go v0.14.0
|
||||||
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387
|
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/cockroachdb/cockroach-go/v2 v2.2.4
|
||||||
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
github.com/eikenb/pipeat v0.0.0-20210603033007-44fc3ffce52b
|
||||||
github.com/fclairamb/ftpserverlib v0.16.0
|
github.com/fclairamb/ftpserverlib v0.16.0
|
||||||
|
@ -25,7 +25,7 @@ require (
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.0
|
github.com/hashicorp/go-retryablehttp v0.7.0
|
||||||
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
github.com/jlaffaye/ftp v0.0.0-20201112195030-9aae4d151126
|
||||||
github.com/klauspost/compress v1.13.6
|
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/lib/pq v1.10.4
|
||||||
github.com/lithammer/shortuuid/v3 v3.0.7
|
github.com/lithammer/shortuuid/v3 v3.0.7
|
||||||
github.com/mattn/go-sqlite3 v1.14.9
|
github.com/mattn/go-sqlite3 v1.14.9
|
||||||
|
@ -53,7 +53,7 @@ require (
|
||||||
gocloud.dev v0.24.0
|
gocloud.dev v0.24.0
|
||||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
||||||
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c
|
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
|
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||||
google.golang.org/api v0.61.0
|
google.golang.org/api v0.61.0
|
||||||
google.golang.org/grpc v1.42.0
|
google.golang.org/grpc v1.42.0
|
||||||
|
@ -62,7 +62,7 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
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/Azure/azure-pipeline-go v0.2.3 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/boombuler/barcode 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/text v0.3.7 // indirect
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||||
google.golang.org/appengine v1.6.7 // 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/ini.v1 v1.66.2 // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // 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.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.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||||
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
|
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.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
|
||||||
cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=
|
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.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.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
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.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.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.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.20 h1:nQkkmTWK5N2Ao1iVzoOx1HTIxwbSWErxyZ1eiwLJWc4=
|
||||||
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/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.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 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=
|
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/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
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.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 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
|
||||||
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
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=
|
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 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
|
||||||
github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
|
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.6/go.mod h1:tJuGuAI3LC71IicTx82Mz1n3w9woAs2bYJZpkjJQ5aU=
|
||||||
github.com/lestrrat-go/jwx v1.2.12 h1:XG2R0T2w5YFBy7Clfionp6cDdDPuo3yU5xFJg5/r91o=
|
github.com/lestrrat-go/jwx v1.2.13 h1:GxuOPfAz4+nzL98WaKxBxEEZ9b7qmyDetMGfBm9yVvE=
|
||||||
github.com/lestrrat-go/jwx v1.2.12/go.mod h1:25DcLbNWArPA/Ew5CcBmewl32cJKxOk5cbepBsIJFzw=
|
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 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
|
||||||
github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
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=
|
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-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-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-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-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E=
|
||||||
golang.org/x/sys v0.0.0-20211204120058-94396e421777/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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=
|
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-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-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-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-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
||||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 h1:fU3FNfL/oBU2D5DvGqiuyVqqn40DdxvaTFHq7aivA3k=
|
google.golang.org/genproto v0.0.0-20211207154714-918901c715cf h1:PSEM+IQFb9xdsj2CGhfqUTfsZvF8DScCVP1QZb2IiTQ=
|
||||||
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
|
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.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/go-chi/render"
|
"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) {
|
func uploadUserFiles(w http.ResponseWriter, r *http.Request) {
|
||||||
if maxUploadFileSize > 0 {
|
if maxUploadFileSize > 0 {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
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))
|
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"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
"github.com/rs/xid"
|
"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 {
|
if maxUploadFileSize > 0 {
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,6 +62,8 @@ const (
|
||||||
userFilePath = "/api/v2/user/file"
|
userFilePath = "/api/v2/user/file"
|
||||||
userFilesPath = "/api/v2/user/files"
|
userFilesPath = "/api/v2/user/files"
|
||||||
userStreamZipPath = "/api/v2/user/streamzip"
|
userStreamZipPath = "/api/v2/user/streamzip"
|
||||||
|
userUploadFilePath = "/api/v2/user/files/upload"
|
||||||
|
userFilesDirsMetadataPath = "/api/v2/user/files/metadata"
|
||||||
apiKeysPath = "/api/v2/apikeys"
|
apiKeysPath = "/api/v2/apikeys"
|
||||||
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
|
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
|
||||||
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
|
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
|
||||||
|
@ -120,6 +122,7 @@ const (
|
||||||
webClientTwoFactorPathDefault = "/web/client/twofactor"
|
webClientTwoFactorPathDefault = "/web/client/twofactor"
|
||||||
webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
|
webClientTwoFactorRecoveryPathDefault = "/web/client/twofactor-recovery"
|
||||||
webClientFilesPathDefault = "/web/client/files"
|
webClientFilesPathDefault = "/web/client/files"
|
||||||
|
webClientFilePathDefault = "/web/client/file"
|
||||||
webClientSharesPathDefault = "/web/client/shares"
|
webClientSharesPathDefault = "/web/client/shares"
|
||||||
webClientSharePathDefault = "/web/client/share"
|
webClientSharePathDefault = "/web/client/share"
|
||||||
webClientEditFilePathDefault = "/web/client/editfile"
|
webClientEditFilePathDefault = "/web/client/editfile"
|
||||||
|
@ -147,6 +150,7 @@ const (
|
||||||
maxMultipartMem = 10485760 // 10 MB
|
maxMultipartMem = 10485760 // 10 MB
|
||||||
osWindows = "windows"
|
osWindows = "windows"
|
||||||
otpHeaderCode = "X-SFTPGO-OTP"
|
otpHeaderCode = "X-SFTPGO-OTP"
|
||||||
|
mTimeHeader = "X-SFTPGO-MTIME"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -195,6 +199,7 @@ var (
|
||||||
webClientTwoFactorPath string
|
webClientTwoFactorPath string
|
||||||
webClientTwoFactorRecoveryPath string
|
webClientTwoFactorRecoveryPath string
|
||||||
webClientFilesPath string
|
webClientFilesPath string
|
||||||
|
webClientFilePath string
|
||||||
webClientSharesPath string
|
webClientSharesPath string
|
||||||
webClientSharePath string
|
webClientSharePath string
|
||||||
webClientEditFilePath string
|
webClientEditFilePath string
|
||||||
|
@ -578,6 +583,7 @@ func updateWebClientURLs(baseURL string) {
|
||||||
webClientTwoFactorPath = path.Join(baseURL, webClientTwoFactorPathDefault)
|
webClientTwoFactorPath = path.Join(baseURL, webClientTwoFactorPathDefault)
|
||||||
webClientTwoFactorRecoveryPath = path.Join(baseURL, webClientTwoFactorRecoveryPathDefault)
|
webClientTwoFactorRecoveryPath = path.Join(baseURL, webClientTwoFactorRecoveryPathDefault)
|
||||||
webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
|
webClientFilesPath = path.Join(baseURL, webClientFilesPathDefault)
|
||||||
|
webClientFilePath = path.Join(baseURL, webClientFilePathDefault)
|
||||||
webClientSharesPath = path.Join(baseURL, webClientSharesPathDefault)
|
webClientSharesPath = path.Join(baseURL, webClientSharesPathDefault)
|
||||||
webClientPubSharesPath = path.Join(baseURL, webClientPubSharesPathDefault)
|
webClientPubSharesPath = path.Join(baseURL, webClientPubSharesPathDefault)
|
||||||
webClientSharePath = path.Join(baseURL, webClientSharePathDefault)
|
webClientSharePath = path.Join(baseURL, webClientSharePathDefault)
|
||||||
|
|
|
@ -91,6 +91,8 @@ const (
|
||||||
userDirsPath = "/api/v2/user/dirs"
|
userDirsPath = "/api/v2/user/dirs"
|
||||||
userFilesPath = "/api/v2/user/files"
|
userFilesPath = "/api/v2/user/files"
|
||||||
userStreamZipPath = "/api/v2/user/streamzip"
|
userStreamZipPath = "/api/v2/user/streamzip"
|
||||||
|
userUploadFilePath = "/api/v2/user/files/upload"
|
||||||
|
userFilesDirsMetadataPath = "/api/v2/user/files/metadata"
|
||||||
apiKeysPath = "/api/v2/apikeys"
|
apiKeysPath = "/api/v2/apikeys"
|
||||||
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
|
adminTOTPConfigsPath = "/api/v2/admin/totp/configs"
|
||||||
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
|
adminTOTPGeneratePath = "/api/v2/admin/totp/generate"
|
||||||
|
@ -8677,6 +8679,14 @@ func TestPreUploadHook(t *testing.T) {
|
||||||
rr := executeRequest(req)
|
rr := executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusCreated, rr)
|
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)
|
err = os.WriteFile(preActionPath, getExitCodeScriptContent(1), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
_, err = reader.Seek(0, io.SeekStart)
|
_, err = reader.Seek(0, io.SeekStart)
|
||||||
|
@ -8688,6 +8698,14 @@ func TestPreUploadHook(t *testing.T) {
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusForbidden, rr)
|
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)
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
|
@ -8929,6 +8947,94 @@ func TestShareUsage(t *testing.T) {
|
||||||
executeRequest(req)
|
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) {
|
func TestShareUncompressed(t *testing.T) {
|
||||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -9985,6 +10091,119 @@ func TestWebDirsAPI(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusNotFound, rr)
|
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) {
|
func TestWebFilesAPI(t *testing.T) {
|
||||||
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -10281,7 +10500,7 @@ func TestWebUploadErrors(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusNotFound, rr)
|
checkResponseCode(t, http.StatusNotFound, rr)
|
||||||
|
|
||||||
if runtime.GOOS != osWindows {
|
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)
|
assert.NoError(t, err)
|
||||||
setBearerForReq(req, webAPIToken)
|
setBearerForReq(req, webAPIToken)
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
|
@ -10301,6 +10520,13 @@ func TestWebUploadErrors(t *testing.T) {
|
||||||
checkResponseCode(t, http.StatusForbidden, rr)
|
checkResponseCode(t, http.StatusForbidden, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "Error closing file")
|
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)
|
err = os.Chmod(user.GetHomeDir(), os.ModePerm)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -10577,6 +10803,30 @@ func TestWebUploadSFTP(t *testing.T) {
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "denying write due to space limit")
|
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
|
// delete the file
|
||||||
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.txt", nil)
|
req, err = http.NewRequest(http.MethodDelete, userFilesPath+"?path=file.txt", nil)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
@ -10593,6 +10843,19 @@ func TestWebUploadSFTP(t *testing.T) {
|
||||||
rr = executeRequest(req)
|
rr = executeRequest(req)
|
||||||
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
checkResponseCode(t, http.StatusRequestEntityTooLarge, rr)
|
||||||
assert.Contains(t, rr.Body.String(), "denying write due to space limit")
|
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)
|
_, err = httpdtest.RemoveUser(sftpUser, http.StatusOK)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
|
@ -81,6 +81,7 @@ func (s *httpdServer) listenAndServe() error {
|
||||||
config := &tls.Config{
|
config := &tls.Config{
|
||||||
GetCertificate: certMgr.GetCertificateFunc(),
|
GetCertificate: certMgr.GetCertificateFunc(),
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"http/1.1", "h2"},
|
||||||
CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
|
CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
}
|
}
|
||||||
|
@ -1004,7 +1005,8 @@ func (s *httpdServer) initializeRouter() {
|
||||||
|
|
||||||
// share API exposed to external users
|
// share API exposed to external users
|
||||||
s.router.Get(sharesPath+"/{id}", downloadFromShare)
|
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.Get(tokenPath, s.getToken)
|
||||||
s.router.Post(adminPath+"/{username}/forgot-password", forgotAdminPassword)
|
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)).Get(userSharesPath+"/{id}", getShareByID)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Put(userSharesPath+"/{id}", updateShare)
|
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Put(userSharesPath+"/{id}", updateShare)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientSharesDisabled)).Delete(userSharesPath+"/{id}", deleteShare)
|
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 {
|
if s.renderOpenAPI {
|
||||||
|
@ -1215,7 +1219,8 @@ func (s *httpdServer) initializeRouter() {
|
||||||
Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
|
Post(webClientTwoFactorRecoveryPath, s.handleWebClientTwoFactorRecoveryPost)
|
||||||
// share API exposed to external users
|
// share API exposed to external users
|
||||||
s.router.Get(webClientPubSharesPath+"/{id}", downloadFromShare)
|
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) {
|
s.router.Group(func(router chi.Router) {
|
||||||
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie))
|
router.Use(jwtauth.Verify(s.tokenAuth, jwtauth.TokenFromCookie))
|
||||||
|
@ -1224,8 +1229,9 @@ func (s *httpdServer) initializeRouter() {
|
||||||
router.Get(webClientLogoutPath, handleWebClientLogout)
|
router.Get(webClientLogoutPath, handleWebClientLogout)
|
||||||
router.With(s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
|
router.With(s.refreshCookie).Get(webClientFilesPath, s.handleClientGetFiles)
|
||||||
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
|
router.With(s.refreshCookie).Get(webClientViewPDFPath, handleClientViewPDF)
|
||||||
|
router.With(s.refreshCookie, verifyCSRFHeader).Get(webClientFilePath, getUserFile)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||||
Post(webClientFilesPath, uploadUserFiles)
|
Post(webClientFilePath, uploadUserFile)
|
||||||
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
|
router.With(s.refreshCookie).Get(webClientEditFilePath, handleClientEditFile)
|
||||||
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
router.With(checkHTTPUserPerm(sdk.WebClientWriteDisabled), verifyCSRFHeader).
|
||||||
Patch(webClientFilesPath, renameUserFile)
|
Patch(webClientFilesPath, renameUserFile)
|
||||||
|
|
|
@ -109,6 +109,7 @@ type viewPDFPage struct {
|
||||||
type editFilePage struct {
|
type editFilePage struct {
|
||||||
baseClientPage
|
baseClientPage
|
||||||
CurrentDir string
|
CurrentDir string
|
||||||
|
FileURL string
|
||||||
Path string
|
Path string
|
||||||
Name string
|
Name string
|
||||||
ReadOnly bool
|
ReadOnly bool
|
||||||
|
@ -121,6 +122,7 @@ type filesPage struct {
|
||||||
DirsURL string
|
DirsURL string
|
||||||
DownloadURL string
|
DownloadURL string
|
||||||
ViewPDFURL string
|
ViewPDFURL string
|
||||||
|
FileURL string
|
||||||
CanAddFiles bool
|
CanAddFiles bool
|
||||||
CanCreateDirs bool
|
CanCreateDirs bool
|
||||||
CanRename bool
|
CanRename bool
|
||||||
|
@ -412,6 +414,7 @@ func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileDa
|
||||||
Path: fileName,
|
Path: fileName,
|
||||||
Name: path.Base(fileName),
|
Name: path.Base(fileName),
|
||||||
CurrentDir: path.Dir(fileName),
|
CurrentDir: path.Dir(fileName),
|
||||||
|
FileURL: webClientFilePath,
|
||||||
ReadOnly: readOnly,
|
ReadOnly: readOnly,
|
||||||
Data: fileData,
|
Data: fileData,
|
||||||
}
|
}
|
||||||
|
@ -447,6 +450,7 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
|
||||||
DownloadURL: webClientDownloadZipPath,
|
DownloadURL: webClientDownloadZipPath,
|
||||||
ViewPDFURL: webClientViewPDFPath,
|
ViewPDFURL: webClientViewPDFPath,
|
||||||
DirsURL: webClientDirsPath,
|
DirsURL: webClientDirsPath,
|
||||||
|
FileURL: webClientFilePath,
|
||||||
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
CanAddFiles: user.CanAddFilesFromWeb(dirName),
|
||||||
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
CanCreateDirs: user.CanAddDirsFromWeb(dirName),
|
||||||
CanRename: user.CanRenameFromWeb(dirName, 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 {
|
for idx := range s.binding.WebClientIntegrations {
|
||||||
if util.IsStringInSlice(extension, s.binding.WebClientIntegrations[idx].FileExtensions) {
|
if util.IsStringInSlice(extension, s.binding.WebClientIntegrations[idx].FileExtensions) {
|
||||||
res["ext_url"] = s.binding.WebClientIntegrations[idx].URL
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,7 +100,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- public shares
|
- public shares
|
||||||
summary: Upload one or more files to the shared path
|
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
|
operationId: upload_to_share
|
||||||
requestBody:
|
requestBody:
|
||||||
content:
|
content:
|
||||||
|
@ -137,6 +137,77 @@ paths:
|
||||||
$ref: '#/components/responses/InternalServerError'
|
$ref: '#/components/responses/InternalServerError'
|
||||||
default:
|
default:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$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:
|
/token:
|
||||||
get:
|
get:
|
||||||
security:
|
security:
|
||||||
|
@ -3701,6 +3772,114 @@ paths:
|
||||||
$ref: '#/components/responses/InternalServerError'
|
$ref: '#/components/responses/InternalServerError'
|
||||||
default:
|
default:
|
||||||
$ref: '#/components/responses/DefaultResponse'
|
$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:
|
/user/streamzip:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -105,6 +105,7 @@ func (c Conf) Initialize(configDir string) error {
|
||||||
config := &tls.Config{
|
config := &tls.Config{
|
||||||
GetCertificate: certMgr.GetCertificateFunc(),
|
GetCertificate: certMgr.GetCertificateFunc(),
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"http/1.1", "h2"},
|
||||||
CipherSuites: util.GetTLSCiphersFromNames(c.TLSCipherSuites),
|
CipherSuites: util.GetTLSCiphersFromNames(c.TLSCipherSuites),
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,7 +155,7 @@ function deleteAction() {
|
||||||
var selectedRows = table.rows({ selected: true }).count();
|
var selectedRows = table.rows({ selected: true }).count();
|
||||||
if (selectedRows == 1){
|
if (selectedRows == 1){
|
||||||
var folderName = table.row({ selected: true }).data()[0];
|
var folderName = table.row({ selected: true }).data()[0];
|
||||||
var path = '{{.FolderTemplateURL}}' + "?from=" + fixedEncodeURIComponent(folderName);
|
var path = '{{.FolderTemplateURL}}' + "?from=" + encodeURIComponent(folderName);
|
||||||
window.location.href = path;
|
window.location.href = path;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '{{.FolderTemplateURL}}';
|
window.location.href = '{{.FolderTemplateURL}}';
|
||||||
|
|
|
@ -159,7 +159,7 @@
|
||||||
titleAttr: "Clone",
|
titleAttr: "Clone",
|
||||||
action: function (e, dt, node, config) {
|
action: function (e, dt, node, config) {
|
||||||
var username = dt.row({ selected: true }).data()[1];
|
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;
|
window.location.href = path;
|
||||||
},
|
},
|
||||||
enabled: false
|
enabled: false
|
||||||
|
@ -172,7 +172,7 @@
|
||||||
var selectedRows = table.rows({ selected: true }).count();
|
var selectedRows = table.rows({ selected: true }).count();
|
||||||
if (selectedRows == 1){
|
if (selectedRows == 1){
|
||||||
var username = dt.row({ selected: true }).data()[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;
|
window.location.href = path;
|
||||||
} else {
|
} else {
|
||||||
window.location.href = '{{.UserTemplateURL}}';
|
window.location.href = '{{.UserTemplateURL}}';
|
||||||
|
|
|
@ -134,44 +134,53 @@
|
||||||
{{if not .ReadOnly}}
|
{{if not .ReadOnly}}
|
||||||
function saveFile() {
|
function saveFile() {
|
||||||
$('#idSave').addClass("disabled");
|
$('#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({
|
async function uploadFile() {
|
||||||
url: path,
|
var errorMessage = "Error saving file";
|
||||||
type: 'POST',
|
let response;
|
||||||
data: data,
|
try {
|
||||||
processData: false,
|
var uploadPath = '{{.FileURL}}?path='+encodeURIComponent('{{.CurrentDir}}/{{.Name}}');
|
||||||
contentType: false,
|
var cm = document.querySelector('.CodeMirror').CodeMirror;
|
||||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
var blob = new Blob([cm.getValue()]);
|
||||||
timeout: 15000,
|
response = await fetch(uploadPath, {
|
||||||
success: function (result) {
|
method: 'POST',
|
||||||
window.location.href = '{{.FilesURL}}?path={{.CurrentDir}}';
|
headers: {
|
||||||
},
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
error: function ($xhr, textStatus, errorThrown) {
|
},
|
||||||
$('#idSave').removeClass("disabled");
|
credentials: 'same-origin',
|
||||||
var txt = "Error saving file";
|
redirect: 'error',
|
||||||
if ($xhr) {
|
body: blob
|
||||||
var json = $xhr.responseJSON;
|
});
|
||||||
if (json) {
|
if (response.status == 201){
|
||||||
if (json.message) {
|
window.location.href = '{{.FilesURL}}?path='+encodeURIComponent('{{.CurrentDir}}');
|
||||||
txt = json.message;
|
} else {
|
||||||
}
|
let jsonResponse;
|
||||||
if (json.error) {
|
try {
|
||||||
txt += ": " + json.error;
|
jsonResponse = await response.json();
|
||||||
}
|
} catch(e){
|
||||||
}
|
throw Error(errorMessage);
|
||||||
}
|
}
|
||||||
$('#errorTxt').text(txt);
|
if (jsonResponse.message) {
|
||||||
$('#errorMsg').show();
|
errorMessage = jsonResponse.message;
|
||||||
setTimeout(function () {
|
}
|
||||||
$('#errorMsg').hide();
|
if (jsonResponse.error) {
|
||||||
}, 5000);
|
errorMessage += ": " + jsonResponse.error;
|
||||||
|
}
|
||||||
|
throw Error(errorMessage);
|
||||||
}
|
}
|
||||||
});
|
} catch (e){
|
||||||
|
throw Error(errorMessage+": " +e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadFile().catch(function(error){
|
||||||
|
$('#idSave').removeClass("disabled");
|
||||||
|
$('#errorTxt').text(error.message);
|
||||||
|
$('#errorMsg').show();
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
{{end}}
|
{{end}}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -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) {
|
if (childReference == null || childReference.closed) {
|
||||||
console.log("external window null or closed, cannot notify save");
|
console.log("external window null or closed, cannot notify save");
|
||||||
return;
|
return;
|
||||||
|
@ -243,70 +255,118 @@
|
||||||
}, childProps.get('url'));
|
}, childProps.get('url'));
|
||||||
break;
|
break;
|
||||||
case 'sendBlob':
|
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
|
// we first send a blobDownloadStart message so the child can
|
||||||
// show a spinner or something similar
|
// show a spinner or something similar
|
||||||
|
var errorMessage = "Error downloading file";
|
||||||
childReference.postMessage({
|
childReference.postMessage({
|
||||||
type: 'blobDownloadStart'
|
type: 'blobDownloadStart'
|
||||||
}, childProps.get('url'));
|
}, childProps.get('url'));
|
||||||
// download the file and send as blob to the child window
|
// download the file and send it as blob to the child window
|
||||||
fetch(childProps.get('link'))
|
async function downloadFileAsBlob(){
|
||||||
.then(response => response.blob())
|
var errorMessage = "Error downloading file";
|
||||||
.then(function(responseBlob){
|
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: ""});
|
let fileBlob = new File([responseBlob], childProps.get('file_name'), {type: responseBlob.type, lastModified: ""});
|
||||||
childReference.postMessage({
|
childReference.postMessage({
|
||||||
type: 'blob',
|
type: 'blob',
|
||||||
file: fileBlob
|
file: fileBlob
|
||||||
}, childProps.get('url'));
|
}, 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;
|
break;
|
||||||
case 'saveBlob':
|
case 'saveBlob':
|
||||||
// get the blob from the message and save it
|
|
||||||
var path = '{{.FilesURL}}?path={{.CurrentDir}}';
|
|
||||||
spinnerDone = false;
|
spinnerDone = false;
|
||||||
var file = new File([event.data.file], childProps.get('file_name'));
|
$('#spinnerModal').modal('show');
|
||||||
var data = new FormData();
|
|
||||||
data.append('filenames', file);
|
|
||||||
|
|
||||||
$.ajax({
|
async function saveBlob() {
|
||||||
url: path,
|
var errorMessage = "Error saving external file";
|
||||||
type: 'POST',
|
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+childProps.get('file_name'));
|
||||||
data: data,
|
let response;
|
||||||
processData: false,
|
try {
|
||||||
contentType: false,
|
response = await fetch(uploadPath, {
|
||||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
method: 'POST',
|
||||||
timeout: 0,
|
headers: {
|
||||||
beforeSend: function () {
|
'X-CSRF-TOKEN': '{{.CSRFToken}}'
|
||||||
$('#spinnerModal').modal('show');
|
},
|
||||||
},
|
credentials: 'same-origin',
|
||||||
success: function (result) {
|
redirect: 'error',
|
||||||
|
body: event.data.file
|
||||||
|
});
|
||||||
|
} catch (e){
|
||||||
|
throw Error(errorMessage+": " +e.message);
|
||||||
|
}
|
||||||
|
if (response.status == 201){
|
||||||
$('#spinnerModal').modal('hide');
|
$('#spinnerModal').modal('hide');
|
||||||
notifySave("OK", "");
|
notifySave("OK", "");
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
location.reload();
|
location.reload();
|
||||||
}, 2000);
|
}, 2000);
|
||||||
},
|
} else {
|
||||||
error: function ($xhr, textStatus, errorThrown) {
|
let jsonResponse;
|
||||||
$('#spinnerModal').modal('hide');
|
try {
|
||||||
var txt = "Error saving external file";
|
jsonResponse = await response.json();
|
||||||
if ($xhr) {
|
} catch(e){
|
||||||
var json = $xhr.responseJSON;
|
throw Error(errorMessage);
|
||||||
if (json) {
|
|
||||||
if (json.message) {
|
|
||||||
txt = json.message;
|
|
||||||
}
|
|
||||||
if (json.error) {
|
|
||||||
txt += ": " + json.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
notifySave("KO", txt);
|
if (jsonResponse.message) {
|
||||||
$('#errorTxt').text(txt);
|
errorMessage = jsonResponse.message;
|
||||||
$('#errorMsg').show();
|
}
|
||||||
setTimeout(function () {
|
if (jsonResponse.error) {
|
||||||
$('#errorMsg').hide();
|
errorMessage += ": " + jsonResponse.error;
|
||||||
}, 5000);
|
}
|
||||||
|
throw Error(errorMessage);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveBlob().catch(function(error){
|
||||||
|
$('#spinnerModal').modal('hide');
|
||||||
|
notifySave("KO", error.message);
|
||||||
|
$('#errorTxt').text(error.message);
|
||||||
|
$('#errorMsg').show();
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
@ -465,7 +525,7 @@
|
||||||
} else {
|
} else {
|
||||||
path = '{{.FilesURL}}';
|
path = '{{.FilesURL}}';
|
||||||
}
|
}
|
||||||
path+='?path={{.CurrentDir}}'+fixedEncodeURIComponent("/"+itemName);
|
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName);
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: path,
|
url: path,
|
||||||
|
@ -526,7 +586,7 @@
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
$('#createDirModal').modal('hide');
|
$('#createDirModal').modal('hide');
|
||||||
var dirName = replaceSlash($("#directory_name").val());
|
var dirName = replaceSlash($("#directory_name").val());
|
||||||
var path = '{{.DirsURL}}?path={{.CurrentDir}}' + fixedEncodeURIComponent("/"+dirName);
|
var path = '{{.DirsURL}}?path={{.CurrentDir}}' + encodeURIComponent("/"+dirName);
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: path,
|
url: path,
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
|
@ -562,7 +622,6 @@
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
keepAlive();
|
keepAlive();
|
||||||
var keepAliveTimer = setInterval(keepAlive, 300000);
|
var keepAliveTimer = setInterval(keepAlive, 300000);
|
||||||
var path = '{{.FilesURL}}?path={{.CurrentDir}}';
|
|
||||||
|
|
||||||
var files = $("#files_name")[0].files;
|
var files = $("#files_name")[0].files;
|
||||||
var has_errors = false;
|
var has_errors = false;
|
||||||
|
@ -584,48 +643,57 @@
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
//console.log("upload file, index: "+index);
|
|
||||||
var data = new FormData();
|
|
||||||
data.append('filenames', files[index]);
|
|
||||||
|
|
||||||
$.ajax({
|
async function saveFile() {
|
||||||
url: path,
|
//console.log("save file, index: "+index);
|
||||||
type: 'POST',
|
var errorMessage = "Error uploading files";
|
||||||
data: data,
|
let response;
|
||||||
processData: false,
|
try {
|
||||||
contentType: false,
|
var f = files[index];
|
||||||
headers: { 'X-CSRF-TOKEN': '{{.CSRFToken}}' },
|
var uploadPath = '{{.FileURL}}?path={{.CurrentDir}}'+encodeURIComponent("/"+f.name);
|
||||||
timeout: 0,
|
response = await fetch(uploadPath, {
|
||||||
success: function (result) {
|
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++;
|
index++;
|
||||||
success++;
|
success++;
|
||||||
uploadFile();
|
uploadFile();
|
||||||
},
|
} else {
|
||||||
error: function ($xhr, textStatus, errorThrown) {
|
let jsonResponse;
|
||||||
index++;
|
try {
|
||||||
has_errors = true;
|
jsonResponse = await response.json();
|
||||||
var txt = "Error uploading files";
|
} catch(e){
|
||||||
if (success > 0){
|
throw Error(errorMessage);
|
||||||
txt = "Not all files have been uploaded, please reload the page";
|
|
||||||
}
|
}
|
||||||
if ($xhr) {
|
if (jsonResponse.message) {
|
||||||
var json = $xhr.responseJSON;
|
errorMessage = jsonResponse.message;
|
||||||
if (json) {
|
|
||||||
if (json.message) {
|
|
||||||
txt = json.message;
|
|
||||||
}
|
|
||||||
if (json.error) {
|
|
||||||
txt += ": " + json.error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
$('#errorTxt').text(txt);
|
if (jsonResponse.error) {
|
||||||
$('#errorMsg').show();
|
errorMessage += ": " + jsonResponse.error;
|
||||||
setTimeout(function () {
|
}
|
||||||
$('#errorMsg').hide();
|
throw Error(errorMessage);
|
||||||
}, 10000);
|
|
||||||
uploadFile();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveFile().catch(function(error){
|
||||||
|
index++;
|
||||||
|
has_errors = true;
|
||||||
|
$('#errorTxt').text(error.message);
|
||||||
|
$('#errorMsg').show();
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#errorMsg').hide();
|
||||||
|
}, 10000);
|
||||||
|
uploadFile();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -646,7 +714,7 @@
|
||||||
} else {
|
} else {
|
||||||
path = '{{.FilesURL}}';
|
path = '{{.FilesURL}}';
|
||||||
}
|
}
|
||||||
path+='?path={{.CurrentDir}}'+fixedEncodeURIComponent("/"+itemName)+'&target={{.CurrentDir}}'+fixedEncodeURIComponent("/"+targetName);
|
path+='?path={{.CurrentDir}}'+encodeURIComponent("/"+itemName)+'&target={{.CurrentDir}}'+encodeURIComponent("/"+targetName);
|
||||||
$('renameModal').modal('hide');
|
$('renameModal').modal('hide');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: path,
|
url: path,
|
||||||
|
@ -698,7 +766,7 @@
|
||||||
for (i = 0; i < selected.length; i++) {
|
for (i = 0; i < selected.length; i++) {
|
||||||
filesArray.push(getNameFromMeta(selected[i]));
|
filesArray.push(getNameFromMeta(selected[i]));
|
||||||
}
|
}
|
||||||
var files = fixedEncodeURIComponent(JSON.stringify(filesArray));
|
var files = encodeURIComponent(JSON.stringify(filesArray));
|
||||||
var downloadURL = '{{.DownloadURL}}';
|
var downloadURL = '{{.DownloadURL}}';
|
||||||
var currentDir = '{{.CurrentDir}}';
|
var currentDir = '{{.CurrentDir}}';
|
||||||
var ts = new Date().getTime().toString();
|
var ts = new Date().getTime().toString();
|
||||||
|
@ -763,7 +831,7 @@
|
||||||
for (i = 0; i < selected.length; i++) {
|
for (i = 0; i < selected.length; i++) {
|
||||||
filesArray.push(getNameFromMeta(selected[i]));
|
filesArray.push(getNameFromMeta(selected[i]));
|
||||||
}
|
}
|
||||||
var files = fixedEncodeURIComponent(JSON.stringify(filesArray));
|
var files = encodeURIComponent(JSON.stringify(filesArray));
|
||||||
var shareURL = '{{.ShareURL}}';
|
var shareURL = '{{.ShareURL}}';
|
||||||
var currentDir = '{{.CurrentDir}}';
|
var currentDir = '{{.CurrentDir}}';
|
||||||
var ts = new Date().getTime().toString();
|
var ts = new Date().getTime().toString();
|
||||||
|
@ -808,6 +876,7 @@
|
||||||
if (!data.sftpgo_dir || data.sftpgo_dir != '{{.CurrentDir}}'){
|
if (!data.sftpgo_dir || data.sftpgo_dir != '{{.CurrentDir}}'){
|
||||||
data.start = 0;
|
data.start = 0;
|
||||||
data.search.search = "";
|
data.search.search = "";
|
||||||
|
data.checkboxes = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"columns": [
|
"columns": [
|
||||||
|
@ -874,7 +943,7 @@
|
||||||
{{if .HasIntegrations}}
|
{{if .HasIntegrations}}
|
||||||
if (type === 'display') {
|
if (type === 'display') {
|
||||||
if (data){
|
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}}
|
{{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>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>For example:</p>
|
||||||
<p><code>curl -F filenames=@file1.txt -F filenames=@file2.txt "share link"</code></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>
|
||||||
<div id="expiredShare">
|
<div id="expiredShare">
|
||||||
This share is no longer accessible because it has expired
|
This share is no longer accessible because it has expired
|
||||||
|
@ -222,6 +225,8 @@
|
||||||
$('#readShare').hide();
|
$('#readShare').hide();
|
||||||
$('#writeLink').attr("href", shareURL);
|
$('#writeLink').attr("href", shareURL);
|
||||||
$('#writeLink').attr("title", shareURL);
|
$('#writeLink').attr("title", shareURL);
|
||||||
|
$('#writeLinkSingle').attr("href", shareURL);
|
||||||
|
$('#writeLinkSingle').attr("title", shareURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$('#linkModal').modal('show');
|
$('#linkModal').modal('show');
|
||||||
|
|
|
@ -59,6 +59,7 @@ func (s *webDavServer) listenAndServe(compressor *middleware.Compressor) error {
|
||||||
httpServer.TLSConfig = &tls.Config{
|
httpServer.TLSConfig = &tls.Config{
|
||||||
GetCertificate: certMgr.GetCertificateFunc(),
|
GetCertificate: certMgr.GetCertificateFunc(),
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
|
NextProtos: []string{"http/1.1", "h2"},
|
||||||
CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
|
CipherSuites: util.GetTLSCiphersFromNames(s.binding.TLSCipherSuites),
|
||||||
PreferServerCipherSuites: true,
|
PreferServerCipherSuites: true,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue