Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a18e8b4bf | |||
| bf8213c1a6 | |||
| cffa734edb |
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import codit_logger "codit/logger"
|
import codit_logger "codit/logger"
|
||||||
|
|
||||||
import "archive/zip"
|
import "archive/zip"
|
||||||
|
import "context"
|
||||||
import "database/sql"
|
import "database/sql"
|
||||||
import "errors"
|
import "errors"
|
||||||
import "fmt"
|
import "fmt"
|
||||||
@@ -59,17 +60,24 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
|||||||
var ok bool
|
var ok bool
|
||||||
var store *db.Store
|
var store *db.Store
|
||||||
var sessionItem models.SSHSession
|
var sessionItem models.SSHSession
|
||||||
|
var multipartReader *multipart.Reader
|
||||||
|
var part *multipart.Part
|
||||||
var sshClient *ssh.Client
|
var sshClient *ssh.Client
|
||||||
var sftpClient *sftp.Client
|
var sftpClient *sftp.Client
|
||||||
|
var stopContextClose func()
|
||||||
var targetDir string
|
var targetDir string
|
||||||
var overwrite bool
|
var overwrite bool
|
||||||
var response sshSessionFileUploadResponse
|
var response sshSessionFileUploadResponse
|
||||||
var files []*multipart.FileHeader
|
|
||||||
var file multipart.File
|
|
||||||
var item sshSessionFileUploadItem
|
var item sshSessionFileUploadItem
|
||||||
var remotePath string
|
var remotePath string
|
||||||
|
var fieldName string
|
||||||
|
var fieldValue string
|
||||||
|
var password string
|
||||||
|
var otpCode string
|
||||||
|
var uploadCount int
|
||||||
|
var opened bool
|
||||||
|
var copied int64
|
||||||
var err error
|
var err error
|
||||||
var i int
|
|
||||||
|
|
||||||
user, ok = middleware.UserFromContext(r.Context())
|
user, ok = middleware.UserFromContext(r.Context())
|
||||||
if !ok || user.Disabled {
|
if !ok || user.Disabled {
|
||||||
@@ -77,61 +85,95 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
r.Body = http.MaxBytesReader(w, r.Body, sshFileTransferMaxUploadBytes)
|
r.Body = http.MaxBytesReader(w, r.Body, sshFileTransferMaxUploadBytes)
|
||||||
err = r.ParseMultipartForm(16 * 1024 * 1024)
|
multipartReader, err = r.MultipartReader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart upload"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart upload"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetDir = strings.TrimSpace(r.FormValue("target_dir"))
|
|
||||||
if targetDir == "" {
|
|
||||||
targetDir = "."
|
targetDir = "."
|
||||||
}
|
for {
|
||||||
overwrite = parseSSHFileTransferBool(r.FormValue("overwrite"))
|
if r.Context().Err() != nil {
|
||||||
if r.MultipartForm != nil {
|
|
||||||
files = r.MultipartForm.File["files"]
|
|
||||||
}
|
|
||||||
if len(files) == 0 {
|
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one file is required"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(files) > sshFileTransferMaxFiles {
|
part, err = multipartReader.NextPart()
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("too many files; max is %d", sshFileTransferMaxFiles)})
|
if err == io.EOF {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
store = api.store(r)
|
|
||||||
sessionItem, err = api.getSSHSessionFileTransferItem(store, user, params["id"])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.writeSSHSessionFileTransferError(w, err)
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart upload"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sshClient, err = api.openSSHSessionFileTransferClient(store, user, sessionItem, r.FormValue("password"), r.FormValue("otp_code"))
|
fieldName = part.FormName()
|
||||||
|
if fieldName != "files" {
|
||||||
|
fieldValue, err = readMultipartFieldValue(part, 64 * 1024)
|
||||||
|
_ = part.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if fieldName == "target_dir" {
|
||||||
|
targetDir = strings.TrimSpace(fieldValue)
|
||||||
|
if targetDir == "" {
|
||||||
|
targetDir = "."
|
||||||
|
}
|
||||||
|
} else if fieldName == "overwrite" {
|
||||||
|
overwrite = parseSSHFileTransferBool(fieldValue)
|
||||||
|
} else if fieldName == "password" {
|
||||||
|
password = fieldValue
|
||||||
|
} else if fieldName == "otp_code" {
|
||||||
|
otpCode = fieldValue
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uploadCount += 1
|
||||||
|
if uploadCount > sshFileTransferMaxFiles {
|
||||||
|
_ = part.Close()
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": fmt.Sprintf("too many files; max is %d", sshFileTransferMaxFiles)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !opened {
|
||||||
|
store = api.store(r)
|
||||||
|
sessionItem, err = api.getSSHSessionFileTransferItem(store, user, params["id"])
|
||||||
|
if err != nil {
|
||||||
|
_ = part.Close()
|
||||||
|
api.writeSSHSessionFileTransferError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sshClient, err = api.openSSHSessionFileTransferClient(store, user, sessionItem, password, otpCode)
|
||||||
|
if err != nil {
|
||||||
|
_ = part.Close()
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stopContextClose = closeSSHClientOnContext(r.Context(), sshClient)
|
||||||
|
defer stopContextClose()
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
sftpClient, err = sftp.NewClient(sshClient)
|
sftpClient, err = sftp.NewClient(sshClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
_ = part.Close()
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sftp subsystem unavailable: " + err.Error()})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "sftp subsystem unavailable: " + err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer sftpClient.Close()
|
defer sftpClient.Close()
|
||||||
for i = 0; i < len(files); i++ {
|
opened = true
|
||||||
item = sshSessionFileUploadItem{Name: path.Base(files[i].Filename), Size: files[i].Size}
|
}
|
||||||
|
item = sshSessionFileUploadItem{Name: path.Base(part.FileName())}
|
||||||
if item.Name == "." || item.Name == "/" || strings.Contains(item.Name, "\x00") {
|
if item.Name == "." || item.Name == "/" || strings.Contains(item.Name, "\x00") {
|
||||||
item.Error = "invalid file name"
|
item.Error = "invalid file name"
|
||||||
response.Failed += 1
|
response.Failed += 1
|
||||||
response.Items = append(response.Items, item)
|
response.Items = append(response.Items, item)
|
||||||
|
_ = part.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
remotePath = path.Join(targetDir, item.Name)
|
remotePath = path.Join(targetDir, item.Name)
|
||||||
item.Path = remotePath
|
item.Path = remotePath
|
||||||
file, err = files[i].Open()
|
copied, err = api.sftpUploadFile(r.Context(), sftpClient, targetDir, item.Name, part, overwrite)
|
||||||
if err == nil {
|
_ = part.Close()
|
||||||
err = api.sftpUploadFile(sftpClient, targetDir, item.Name, file, overwrite)
|
item.Size = copied
|
||||||
_ = file.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if r.Context().Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
item.Error = err.Error()
|
item.Error = err.Error()
|
||||||
response.Failed += 1
|
response.Failed += 1
|
||||||
} else {
|
} else {
|
||||||
@@ -139,6 +181,10 @@ func (api *API) UploadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
response.Items = append(response.Items, item)
|
response.Items = append(response.Items, item)
|
||||||
}
|
}
|
||||||
|
if uploadCount == 0 {
|
||||||
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one file is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
WriteJSON(w, http.StatusOK, response)
|
WriteJSON(w, http.StatusOK, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,11 +196,13 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
|||||||
var sessionItem models.SSHSession
|
var sessionItem models.SSHSession
|
||||||
var sshClient *ssh.Client
|
var sshClient *ssh.Client
|
||||||
var sftpClient *sftp.Client
|
var sftpClient *sftp.Client
|
||||||
|
var stopContextClose func()
|
||||||
var items []sshSessionFileDownloadItem
|
var items []sshSessionFileDownloadItem
|
||||||
var item sshSessionFileDownloadItem
|
var item sshSessionFileDownloadItem
|
||||||
var remotePath string
|
var remotePath string
|
||||||
var totalSize int64
|
var totalSize int64
|
||||||
var contentDisposition string
|
var contentDisposition string
|
||||||
|
var archiveName string
|
||||||
var err error
|
var err error
|
||||||
var i int
|
var i int
|
||||||
|
|
||||||
@@ -187,6 +235,8 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
|||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
stopContextClose = closeSSHClientOnContext(r.Context(), sshClient)
|
||||||
|
defer stopContextClose()
|
||||||
defer sshClient.Close()
|
defer sshClient.Close()
|
||||||
sftpClient, err = sftp.NewClient(sshClient)
|
sftpClient, err = sftp.NewClient(sshClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -195,6 +245,9 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
|||||||
}
|
}
|
||||||
defer sftpClient.Close()
|
defer sftpClient.Close()
|
||||||
for i = 0; i < len(req.Paths); i++ {
|
for i = 0; i < len(req.Paths); i++ {
|
||||||
|
if r.Context().Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
remotePath = strings.TrimSpace(req.Paths[i])
|
remotePath = strings.TrimSpace(req.Paths[i])
|
||||||
if remotePath == "" || strings.Contains(remotePath, "\x00") {
|
if remotePath == "" || strings.Contains(remotePath, "\x00") {
|
||||||
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid download path"})
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid download path"})
|
||||||
@@ -216,16 +269,18 @@ func (api *API) DownloadSSHSessionFilesForSelf(w http.ResponseWriter, r *http.Re
|
|||||||
contentDisposition = mime.FormatMediaType("attachment", map[string]string{"filename": items[0].Name})
|
contentDisposition = mime.FormatMediaType("attachment", map[string]string{"filename": items[0].Name})
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
w.Header().Set("Content-Type", "application/octet-stream")
|
||||||
w.Header().Set("Content-Disposition", contentDisposition)
|
w.Header().Set("Content-Disposition", contentDisposition)
|
||||||
err = api.sftpDownloadFile(sftpClient, items[0].RemotePath, w)
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", items[0].Size))
|
||||||
|
err = api.sftpDownloadFile(r.Context(), sftpClient, items[0].RemotePath, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file download failed session=%s path=%q err=%s", sessionItem.ID, items[0].RemotePath, err.Error())
|
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file download failed session=%s path=%q err=%s", sessionItem.ID, items[0].RemotePath, err.Error())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
contentDisposition = mime.FormatMediaType("attachment", map[string]string{"filename": fmt.Sprintf("ssh-session-%s-files.zip", sessionItem.ID)})
|
archiveName = sshSessionFilesArchiveName(store, sessionItem)
|
||||||
|
contentDisposition = mime.FormatMediaType("attachment", map[string]string{"filename": archiveName})
|
||||||
w.Header().Set("Content-Type", "application/zip")
|
w.Header().Set("Content-Type", "application/zip")
|
||||||
w.Header().Set("Content-Disposition", contentDisposition)
|
w.Header().Set("Content-Disposition", contentDisposition)
|
||||||
err = api.writeSSHSessionFileZip(sftpClient, items, w)
|
err = api.writeSSHSessionFileZip(r.Context(), sftpClient, items, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file zip download failed session=%s err=%s", sessionItem.ID, err.Error())
|
api.Logger.Write(logIDSSHBroker, codit_logger.LOG_WARN, "ssh file zip download failed session=%s err=%s", sessionItem.ID, err.Error())
|
||||||
}
|
}
|
||||||
@@ -326,15 +381,46 @@ func (api *API) pinSSHSessionFileTransferHostKey(store *db.Store, sessionItem mo
|
|||||||
return fmt.Errorf("failed to pin host key for %s: %s", profile.Server.Name, err.Error())
|
return fmt.Errorf("failed to pin host key for %s: %s", profile.Server.Name, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) sftpUploadFile(client *sftp.Client, targetDir string, name string, reader io.Reader, overwrite bool) error {
|
func closeSSHClientOnContext(ctx context.Context, client *ssh.Client) func() {
|
||||||
|
var done chan struct{}
|
||||||
|
var stop func()
|
||||||
|
|
||||||
|
done = make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
_ = client.Close()
|
||||||
|
case <-done:
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
stop = func() {
|
||||||
|
close(done)
|
||||||
|
}
|
||||||
|
return stop
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) sftpUploadFile(ctx context.Context, client *sftp.Client, targetDir string, name string, reader io.Reader, overwrite bool) (int64, error) {
|
||||||
var remotePath string
|
var remotePath string
|
||||||
var flags int
|
var flags int
|
||||||
var remoteFile *sftp.File
|
var remoteFile *sftp.File
|
||||||
|
var copied int64
|
||||||
|
var statErr error
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
if ctx.Err() != nil { return 0, ctx.Err() }
|
||||||
err = client.MkdirAll(targetDir)
|
err = client.MkdirAll(targetDir)
|
||||||
if err != nil { return err }
|
if err != nil { return 0, err }
|
||||||
|
if ctx.Err() != nil { return 0, ctx.Err() }
|
||||||
remotePath = path.Join(targetDir, name)
|
remotePath = path.Join(targetDir, name)
|
||||||
|
if !overwrite {
|
||||||
|
_, statErr = client.Stat(remotePath)
|
||||||
|
if statErr == nil {
|
||||||
|
return 0, fmt.Errorf("remote file already exists: %s", remotePath)
|
||||||
|
}
|
||||||
|
if !isSFTPNoSuchFile(statErr) {
|
||||||
|
return 0, statErr
|
||||||
|
}
|
||||||
|
}
|
||||||
flags = os.O_WRONLY | os.O_CREATE
|
flags = os.O_WRONLY | os.O_CREATE
|
||||||
if overwrite {
|
if overwrite {
|
||||||
flags = flags | os.O_TRUNC
|
flags = flags | os.O_TRUNC
|
||||||
@@ -342,10 +428,26 @@ func (api *API) sftpUploadFile(client *sftp.Client, targetDir string, name strin
|
|||||||
flags = flags | os.O_EXCL
|
flags = flags | os.O_EXCL
|
||||||
}
|
}
|
||||||
remoteFile, err = client.OpenFile(remotePath, flags)
|
remoteFile, err = client.OpenFile(remotePath, flags)
|
||||||
if err != nil { return err }
|
if err != nil { return 0, err }
|
||||||
defer remoteFile.Close()
|
defer remoteFile.Close()
|
||||||
_, err = io.Copy(remoteFile, reader)
|
copied, err = copyWithContext(ctx, remoteFile, reader)
|
||||||
return err
|
return copied, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func isSFTPNoSuchFile(err error) bool {
|
||||||
|
var statusErr *sftp.StatusError
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
statusErr = nil
|
||||||
|
if errors.As(err, &statusErr) && statusErr.FxCode() == sftp.ErrSSHFxNoSuchFile {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) sftpDownloadMetadata(client *sftp.Client, remotePath string) (sshSessionFileDownloadItem, error) {
|
func (api *API) sftpDownloadMetadata(client *sftp.Client, remotePath string) (sshSessionFileDownloadItem, error) {
|
||||||
@@ -369,18 +471,50 @@ func (api *API) sftpDownloadMetadata(client *sftp.Client, remotePath string) (ss
|
|||||||
return item, nil
|
return item, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) sftpDownloadFile(client *sftp.Client, remotePath string, writer io.Writer) error {
|
func sshSessionFilesArchiveName(store *db.Store, sessionItem models.SSHSession) string {
|
||||||
|
var profile models.SSHAccessProfile
|
||||||
|
var server models.SSHServer
|
||||||
|
var profileName string
|
||||||
|
var serverName string
|
||||||
|
var timestamp string
|
||||||
|
var err error
|
||||||
|
|
||||||
|
profileName = strings.TrimSpace(sessionItem.ProfileID)
|
||||||
|
serverName = strings.TrimSpace(sessionItem.Host)
|
||||||
|
if sessionItem.Port > 0 && serverName != "" {
|
||||||
|
serverName = fmt.Sprintf("%s-%d", serverName, sessionItem.Port)
|
||||||
|
}
|
||||||
|
profile, err = store.GetSSHAccessProfile(sessionItem.ProfileID)
|
||||||
|
if err == nil && strings.TrimSpace(profile.Name) != "" {
|
||||||
|
profileName = strings.TrimSpace(profile.Name)
|
||||||
|
}
|
||||||
|
server, err = store.GetSSHServer(sessionItem.ServerID)
|
||||||
|
if err == nil && strings.TrimSpace(server.Name) != "" {
|
||||||
|
serverName = strings.TrimSpace(server.Name)
|
||||||
|
}
|
||||||
|
if profileName == "" {
|
||||||
|
profileName = "ssh-profile"
|
||||||
|
}
|
||||||
|
if serverName == "" {
|
||||||
|
serverName = "ssh-server"
|
||||||
|
}
|
||||||
|
timestamp = time.Now().Format("20060102-150405")
|
||||||
|
return fmt.Sprintf("%s-%s-%s-files.zip", sanitizeBundleName(profileName), sanitizeBundleName(serverName), timestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *API) sftpDownloadFile(ctx context.Context, client *sftp.Client, remotePath string, writer io.Writer) error {
|
||||||
var remoteFile *sftp.File
|
var remoteFile *sftp.File
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
if ctx.Err() != nil { return ctx.Err() }
|
||||||
remoteFile, err = client.Open(remotePath)
|
remoteFile, err = client.Open(remotePath)
|
||||||
if err != nil { return err }
|
if err != nil { return err }
|
||||||
defer remoteFile.Close()
|
defer remoteFile.Close()
|
||||||
_, err = io.Copy(writer, remoteFile)
|
_, err = copyWithContext(ctx, writer, remoteFile)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) writeSSHSessionFileZip(client *sftp.Client, items []sshSessionFileDownloadItem, writer io.Writer) error {
|
func (api *API) writeSSHSessionFileZip(ctx context.Context, client *sftp.Client, items []sshSessionFileDownloadItem, writer io.Writer) error {
|
||||||
var zipWriter *zip.Writer
|
var zipWriter *zip.Writer
|
||||||
var zipFile io.Writer
|
var zipFile io.Writer
|
||||||
var closeErr error
|
var closeErr error
|
||||||
@@ -389,9 +523,13 @@ func (api *API) writeSSHSessionFileZip(client *sftp.Client, items []sshSessionFi
|
|||||||
|
|
||||||
zipWriter = zip.NewWriter(writer)
|
zipWriter = zip.NewWriter(writer)
|
||||||
for i = 0; i < len(items); i++ {
|
for i = 0; i < len(items); i++ {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
err = ctx.Err()
|
||||||
|
break
|
||||||
|
}
|
||||||
zipFile, err = zipWriter.Create(sshDownloadEntryName(items[i], i))
|
zipFile, err = zipWriter.Create(sshDownloadEntryName(items[i], i))
|
||||||
if err != nil { break }
|
if err != nil { break }
|
||||||
err = api.sftpDownloadFile(client, items[i].RemotePath, zipFile)
|
err = api.sftpDownloadFile(ctx, client, items[i].RemotePath, zipFile)
|
||||||
if err != nil { break }
|
if err != nil { break }
|
||||||
}
|
}
|
||||||
closeErr = zipWriter.Close()
|
closeErr = zipWriter.Close()
|
||||||
@@ -401,6 +539,52 @@ func (api *API) writeSSHSessionFileZip(client *sftp.Client, items []sshSessionFi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyWithContext(ctx context.Context, writer io.Writer, reader io.Reader) (int64, error) {
|
||||||
|
var buffer []byte
|
||||||
|
var written int64
|
||||||
|
var nr int
|
||||||
|
var nw int
|
||||||
|
var er error
|
||||||
|
var ew error
|
||||||
|
|
||||||
|
buffer = make([]byte, 64 * 1024)
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil { return written, ctx.Err() }
|
||||||
|
nr, er = reader.Read(buffer)
|
||||||
|
if nr > 0 {
|
||||||
|
if ctx.Err() != nil { return written, ctx.Err() }
|
||||||
|
nw, ew = writer.Write(buffer[0:nr])
|
||||||
|
if nw > 0 {
|
||||||
|
written += int64(nw)
|
||||||
|
}
|
||||||
|
if ew != nil {
|
||||||
|
return written, ew
|
||||||
|
}
|
||||||
|
if nr != nw {
|
||||||
|
return written, io.ErrShortWrite
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if er != nil {
|
||||||
|
if er == io.EOF {
|
||||||
|
return written, nil
|
||||||
|
}
|
||||||
|
return written, er
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMultipartFieldValue(part *multipart.Part, maxBytes int64) (string, error) {
|
||||||
|
var data []byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
data, err = io.ReadAll(io.LimitReader(part, maxBytes + 1))
|
||||||
|
if err != nil { return "", err }
|
||||||
|
if int64(len(data)) > maxBytes {
|
||||||
|
return "", errors.New("multipart field is too large")
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
func sshDownloadEntryName(item sshSessionFileDownloadItem, index int) string {
|
func sshDownloadEntryName(item sshSessionFileDownloadItem, index int) string {
|
||||||
var name string
|
var name string
|
||||||
|
|
||||||
|
|||||||
+33
-4
@@ -1172,6 +1172,13 @@ export interface BinaryDownload {
|
|||||||
contentType: string
|
contentType: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StreamingDownload {
|
||||||
|
response: Response
|
||||||
|
filename: string
|
||||||
|
contentType: string
|
||||||
|
contentLength: number
|
||||||
|
}
|
||||||
|
|
||||||
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@@ -1241,6 +1248,26 @@ async function requestBinaryDownload(path: string, fallbackFilename: string, opt
|
|||||||
return { data, filename, contentType }
|
return { data, filename, contentType }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function requestStreamingDownload(path: string, fallbackFilename: string, options: RequestInit = {}): Promise<StreamingDownload> {
|
||||||
|
const res: Response = await fetch(path, {
|
||||||
|
credentials: 'include',
|
||||||
|
...options
|
||||||
|
})
|
||||||
|
let error: { error?: string }
|
||||||
|
let filename: string
|
||||||
|
let contentType: string
|
||||||
|
let contentLength: number
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
error = await res.json().catch(() => ({ error: res.statusText }))
|
||||||
|
throw new Error(error.error || 'Request failed')
|
||||||
|
}
|
||||||
|
filename = filenameFromContentDisposition(res.headers.get('Content-Disposition'), fallbackFilename)
|
||||||
|
contentType = res.headers.get('Content-Type') || 'application/octet-stream'
|
||||||
|
contentLength = Number(res.headers.get('Content-Length') || '0')
|
||||||
|
return { response: res, filename, contentType, contentLength }
|
||||||
|
}
|
||||||
|
|
||||||
async function requestText(path: string, options: RequestInit = {}): Promise<string> {
|
async function requestText(path: string, options: RequestInit = {}): Promise<string> {
|
||||||
const res = await fetch(path, {
|
const res = await fetch(path, {
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
@@ -1539,10 +1566,12 @@ export const api = {
|
|||||||
getSSHSessionTranscriptForSelf: (id: string, options?: RequestInit) => requestText(`/api/ssh/sessions/${id}/transcript`, options),
|
getSSHSessionTranscriptForSelf: (id: string, options?: RequestInit) => requestText(`/api/ssh/sessions/${id}/transcript`, options),
|
||||||
getSSHSessionTranscriptAdmin: (id: string, options?: RequestInit) => requestText(`/api/admin/ssh/sessions/${id}/transcript`, options),
|
getSSHSessionTranscriptAdmin: (id: string, options?: RequestInit) => requestText(`/api/admin/ssh/sessions/${id}/transcript`, options),
|
||||||
disconnectSSHSessionForSelf: (id: string) => request<void>(`/api/ssh/sessions/${id}/disconnect`, { method: 'POST' }),
|
disconnectSSHSessionForSelf: (id: string) => request<void>(`/api/ssh/sessions/${id}/disconnect`, { method: 'POST' }),
|
||||||
uploadSSHSessionFilesForSelf: (id: string, form: FormData) =>
|
uploadSSHSessionFilesForSelf: (id: string, form: FormData, options?: RequestInit) =>
|
||||||
requestForm<SSHSessionFileUploadResponse>(`/api/ssh/sessions/${id}/files/upload`, { method: 'POST', body: form }),
|
requestForm<SSHSessionFileUploadResponse>(`/api/ssh/sessions/${id}/files/upload`, { method: 'POST', body: form, ...(options || {}) }),
|
||||||
downloadSSHSessionFilesForSelf: (id: string, payload: SSHSessionFileDownloadPayload) =>
|
downloadSSHSessionFilesForSelf: (id: string, payload: SSHSessionFileDownloadPayload, options?: RequestInit) =>
|
||||||
requestBinaryDownload(`/api/ssh/sessions/${id}/files/download`, `ssh-session-${id}-files`, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' } }),
|
requestBinaryDownload(`/api/ssh/sessions/${id}/files/download`, `ssh-session-${id}-files`, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, ...(options || {}) }),
|
||||||
|
streamSSHSessionFilesForSelf: (id: string, payload: SSHSessionFileDownloadPayload, options?: RequestInit) =>
|
||||||
|
requestStreamingDownload(`/api/ssh/sessions/${id}/files/download`, `ssh-session-${id}-files`, { method: 'POST', body: JSON.stringify(payload), headers: { 'Content-Type': 'application/json' }, ...(options || {}) }),
|
||||||
inspectSSHCertificate: (certificate: string) =>
|
inspectSSHCertificate: (certificate: string) =>
|
||||||
request<{ dump: string }>('/api/ssh/cert/inspect', { method: 'POST', body: JSON.stringify({ certificate }) }),
|
request<{ dump: string }>('/api/ssh/cert/inspect', { method: 'POST', body: JSON.stringify({ certificate }) }),
|
||||||
signSSHUserKeyForSelf: (id: string, payload: SSHUserCASelfSignPayload) =>
|
signSSHUserKeyForSelf: (id: string, payload: SSHUserCASelfSignPayload) =>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import MuiDialog from '@mui/material/Dialog'
|
||||||
|
import { DialogProps } from '@mui/material/Dialog'
|
||||||
|
|
||||||
|
export default function ModalDialog(props: DialogProps) {
|
||||||
|
return (
|
||||||
|
<MuiDialog
|
||||||
|
{...props}
|
||||||
|
onClose={(event, reason) => {
|
||||||
|
if (reason === 'backdropClick') return
|
||||||
|
props.onClose?.(event, reason)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import DownloadIcon from '@mui/icons-material/Download'
|
import DownloadIcon from '@mui/icons-material/Download'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Link from '@mui/material/Link'
|
import Link from '@mui/material/Link'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Tab from '@mui/material/Tab'
|
import Tab from '@mui/material/Tab'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import ContentCopyIcon from '@mui/icons-material/ContentCopy'
|
|||||||
import VisibilityIcon from '@mui/icons-material/Visibility'
|
import VisibilityIcon from '@mui/icons-material/Visibility'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Link from '@mui/material/Link'
|
import Link from '@mui/material/Link'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Paper from '@mui/material/Paper'
|
import Paper from '@mui/material/Paper'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import List from '@mui/material/List'
|
import List from '@mui/material/List'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import List from '@mui/material/List'
|
import List from '@mui/material/List'
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import MenuItem from '@mui/material/MenuItem'
|
import MenuItem from '@mui/material/MenuItem'
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogContent from '@mui/material/DialogContent'
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import LinearProgress from '@mui/material/LinearProgress'
|
import LinearProgress from '@mui/material/LinearProgress'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { ChangeEvent as ReactChangeEvent, useEffect, useState } from 'react'
|
import { ChangeEvent as ReactChangeEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { api, BinaryDownload } from '../api'
|
import { api, StreamingDownload } from '../api'
|
||||||
import PageAlert from './PageAlert'
|
import PageAlert from './PageAlert'
|
||||||
|
|
||||||
type SSHSessionFileDownloadDialogProps = {
|
type SSHSessionFileDownloadDialogProps = {
|
||||||
@@ -19,6 +19,13 @@ type SSHSessionFileDownloadDialogProps = {
|
|||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SaveFileWindow = Window & {
|
||||||
|
showSaveFilePicker?: (options?: {
|
||||||
|
suggestedName?: string
|
||||||
|
types?: Array<{ description: string; accept: Record<string, string[]> }>
|
||||||
|
}) => Promise<{ createWritable: () => Promise<WritableStream<Uint8Array>> }>
|
||||||
|
}
|
||||||
|
|
||||||
export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownloadDialogProps) {
|
export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownloadDialogProps) {
|
||||||
const [pathsText, setPathsText] = useState('')
|
const [pathsText, setPathsText] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -26,6 +33,7 @@ export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownlo
|
|||||||
const [busy, setBusy] = useState(false)
|
const [busy, setBusy] = useState(false)
|
||||||
const [progressText, setProgressText] = useState('')
|
const [progressText, setProgressText] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const abortRef = useRef<AbortController | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.open) return
|
if (!props.open) return
|
||||||
@@ -37,7 +45,11 @@ export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownlo
|
|||||||
}, [props.open])
|
}, [props.open])
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (busy) return
|
if (busy) {
|
||||||
|
abortRef.current?.abort()
|
||||||
|
setProgressText('Canceling download...')
|
||||||
|
return
|
||||||
|
}
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,25 +58,54 @@ export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownlo
|
|||||||
.split('\n')
|
.split('\n')
|
||||||
.map((item: string) => item.trim())
|
.map((item: string) => item.trim())
|
||||||
.filter((item: string) => item !== '')
|
.filter((item: string) => item !== '')
|
||||||
let downloadResult: BinaryDownload
|
const controller: AbortController = new AbortController()
|
||||||
|
let downloadResult: StreamingDownload
|
||||||
let blob: Blob
|
let blob: Blob
|
||||||
let url: string
|
let url: string
|
||||||
let link: HTMLAnchorElement
|
let link: HTMLAnchorElement
|
||||||
|
let saveWindow: SaveFileWindow
|
||||||
|
let fileHandle: { createWritable: () => Promise<WritableStream<Uint8Array>> } | null
|
||||||
|
let writable: WritableStream<Uint8Array>
|
||||||
|
let data: ArrayBuffer
|
||||||
|
let canUseSavePicker: boolean
|
||||||
|
|
||||||
if (paths.length === 0) {
|
if (paths.length === 0) {
|
||||||
setError('Enter at least one remote file path')
|
setError('Enter at least one remote file path')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
saveWindow = window as SaveFileWindow
|
||||||
|
fileHandle = null
|
||||||
|
canUseSavePicker = Boolean(window.isSecureContext && saveWindow.showSaveFilePicker)
|
||||||
|
if (canUseSavePicker) {
|
||||||
|
try {
|
||||||
|
fileHandle = await saveWindow.showSaveFilePicker?.({
|
||||||
|
suggestedName: suggestedSSHDownloadName(paths, props.sessionID)
|
||||||
|
}) || null
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.name === 'AbortError') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to open the browser save dialog')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
|
abortRef.current = controller
|
||||||
setProgressText(`Preparing ${paths.length} remote file(s)...`)
|
setProgressText(`Preparing ${paths.length} remote file(s)...`)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
downloadResult = await api.downloadSSHSessionFilesForSelf(props.sessionID, {
|
downloadResult = await api.streamSSHSessionFilesForSelf(props.sessionID, {
|
||||||
paths,
|
paths,
|
||||||
password: props.passwordRequired ? password : undefined,
|
password: props.passwordRequired ? password : undefined,
|
||||||
otp_code: props.otpRequired ? otpCode : undefined
|
otp_code: props.otpRequired ? otpCode : undefined
|
||||||
})
|
}, { signal: controller.signal })
|
||||||
blob = new Blob([downloadResult.data], { type: downloadResult.contentType })
|
setProgressText(downloadResult.contentLength > 0 ? `Downloading ${downloadResult.filename}...` : `Streaming ${downloadResult.filename}...`)
|
||||||
|
if (fileHandle && downloadResult.response.body) {
|
||||||
|
writable = await fileHandle.createWritable()
|
||||||
|
await downloadResult.response.body.pipeTo(writable, { signal: controller.signal })
|
||||||
|
} else {
|
||||||
|
data = await downloadResult.response.arrayBuffer()
|
||||||
|
blob = new Blob([data], { type: downloadResult.contentType })
|
||||||
url = window.URL.createObjectURL(blob)
|
url = window.URL.createObjectURL(blob)
|
||||||
link = document.createElement('a')
|
link = document.createElement('a')
|
||||||
link.href = url
|
link.href = url
|
||||||
@@ -73,12 +114,20 @@ export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownlo
|
|||||||
link.click()
|
link.click()
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
|
}
|
||||||
setProgressText('Download started.')
|
setProgressText('Download started.')
|
||||||
props.onClose()
|
props.onClose()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.name === 'AbortError') {
|
||||||
|
setProgressText('Download canceled.')
|
||||||
|
} else {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to download files')
|
setError(err instanceof Error ? err.message : 'Failed to download files')
|
||||||
setProgressText('')
|
setProgressText('')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (abortRef.current === controller) {
|
||||||
|
abortRef.current = null
|
||||||
|
}
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,8 +175,26 @@ export default function SSHSessionFileDownloadDialog(props: SSHSessionFileDownlo
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => void download()} disabled={busy} variant="contained">Download</Button>
|
<Button onClick={() => void download()} disabled={busy} variant="contained">Download</Button>
|
||||||
<Button onClick={close} disabled={busy}>Close</Button>
|
<Button onClick={close}>{busy ? 'Cancel' : 'Close'}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function suggestedSSHDownloadName(paths: string[], sessionID: string): string {
|
||||||
|
let name: string
|
||||||
|
let slashIndex: number
|
||||||
|
|
||||||
|
if (paths.length === 1) {
|
||||||
|
name = paths[0].trim()
|
||||||
|
slashIndex = name.lastIndexOf('/')
|
||||||
|
if (slashIndex >= 0) {
|
||||||
|
name = name.substring(slashIndex + 1)
|
||||||
|
}
|
||||||
|
name = name.trim()
|
||||||
|
if (name !== '') {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `ssh-session-${sessionID}-files.zip`
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogContent from '@mui/material/DialogContent'
|
import DialogContent from '@mui/material/DialogContent'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
@@ -9,7 +9,7 @@ import FormControlLabel from '@mui/material/FormControlLabel'
|
|||||||
import LinearProgress from '@mui/material/LinearProgress'
|
import LinearProgress from '@mui/material/LinearProgress'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
import { ChangeEvent as ReactChangeEvent, useEffect, useState } from 'react'
|
import { ChangeEvent as ReactChangeEvent, useEffect, useRef, useState } from 'react'
|
||||||
import { api, SSHSessionFileUploadItem, SSHSessionFileUploadResponse } from '../api'
|
import { api, SSHSessionFileUploadItem, SSHSessionFileUploadResponse } from '../api'
|
||||||
import PageAlert from './PageAlert'
|
import PageAlert from './PageAlert'
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ export default function SSHSessionFileUploadDialog(props: SSHSessionFileUploadDi
|
|||||||
const [progressText, setProgressText] = useState('')
|
const [progressText, setProgressText] = useState('')
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
const [result, setResult] = useState<SSHSessionFileUploadResponse | null>(null)
|
const [result, setResult] = useState<SSHSessionFileUploadResponse | null>(null)
|
||||||
|
const abortRef = useRef<AbortController | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!props.open) return
|
if (!props.open) return
|
||||||
@@ -55,7 +56,11 @@ export default function SSHSessionFileUploadDialog(props: SSHSessionFileUploadDi
|
|||||||
}, [props.open])
|
}, [props.open])
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
if (busy) return
|
if (busy) {
|
||||||
|
abortRef.current?.abort()
|
||||||
|
setProgressText('Canceling upload...')
|
||||||
|
return
|
||||||
|
}
|
||||||
props.onClose()
|
props.onClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +74,7 @@ export default function SSHSessionFileUploadDialog(props: SSHSessionFileUploadDi
|
|||||||
|
|
||||||
const upload = async () => {
|
const upload = async () => {
|
||||||
const form: FormData = new FormData()
|
const form: FormData = new FormData()
|
||||||
|
const controller: AbortController = new AbortController()
|
||||||
let response: SSHSessionFileUploadResponse
|
let response: SSHSessionFileUploadResponse
|
||||||
let i: number
|
let i: number
|
||||||
|
|
||||||
@@ -84,16 +90,28 @@ export default function SSHSessionFileUploadDialog(props: SSHSessionFileUploadDi
|
|||||||
form.append('files', files[i])
|
form.append('files', files[i])
|
||||||
}
|
}
|
||||||
setBusy(true)
|
setBusy(true)
|
||||||
|
abortRef.current = controller
|
||||||
setProgressText(`Uploading ${files.length} file(s) to ${targetDir.trim() || '.'}...`)
|
setProgressText(`Uploading ${files.length} file(s) to ${targetDir.trim() || '.'}...`)
|
||||||
setError(null)
|
setError(null)
|
||||||
try {
|
try {
|
||||||
response = await api.uploadSSHSessionFilesForSelf(props.sessionID, form)
|
response = await api.uploadSSHSessionFilesForSelf(props.sessionID, form, { signal: controller.signal })
|
||||||
|
if (response.failed === 0) {
|
||||||
|
props.onClose()
|
||||||
|
return
|
||||||
|
}
|
||||||
setResult(response)
|
setResult(response)
|
||||||
setProgressText(response.failed > 0 ? 'Upload completed with errors.' : 'Upload completed.')
|
setProgressText('Upload completed with errors.')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof Error && err.name === 'AbortError') {
|
||||||
|
setProgressText('Upload canceled.')
|
||||||
|
} else {
|
||||||
setError(err instanceof Error ? err.message : 'Failed to upload files')
|
setError(err instanceof Error ? err.message : 'Failed to upload files')
|
||||||
setProgressText('')
|
setProgressText('')
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
if (abortRef.current === controller) {
|
||||||
|
abortRef.current = null
|
||||||
|
}
|
||||||
setBusy(false)
|
setBusy(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -175,7 +193,7 @@ export default function SSHSessionFileUploadDialog(props: SSHSessionFileUploadDi
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => void upload()} disabled={busy || files.length === 0} variant="contained">Upload</Button>
|
<Button onClick={() => void upload()} disabled={busy || files.length === 0} variant="contained">Upload</Button>
|
||||||
<Button onClick={close} disabled={busy}>Close</Button>
|
<Button onClick={close}>{busy ? 'Cancel' : 'Close'}</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Typography from '@mui/material/Typography'
|
import Typography from '@mui/material/Typography'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import DownloadIcon from '@mui/icons-material/Download'
|
import DownloadIcon from '@mui/icons-material/Download'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import TextField from '@mui/material/TextField'
|
import TextField from '@mui/material/TextField'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import Alert from '@mui/material/Alert'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from './ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Alert from '@mui/material/Alert'
|
import Alert from '@mui/material/Alert'
|
||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '../components/ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import Link from '@mui/material/Link'
|
import Link from '@mui/material/Link'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Box from '@mui/material/Box'
|
|||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '../components/ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '../components/ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Box from '@mui/material/Box'
|
|||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Checkbox from '@mui/material/Checkbox'
|
import Checkbox from '@mui/material/Checkbox'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '../components/ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import DialogTitle from '@mui/material/DialogTitle'
|
import DialogTitle from '@mui/material/DialogTitle'
|
||||||
import FormControlLabel from '@mui/material/FormControlLabel'
|
import FormControlLabel from '@mui/material/FormControlLabel'
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import '@xterm/xterm/css/xterm.css'
|
|||||||
import Box from '@mui/material/Box'
|
import Box from '@mui/material/Box'
|
||||||
import Button from '@mui/material/Button'
|
import Button from '@mui/material/Button'
|
||||||
import Chip from '@mui/material/Chip'
|
import Chip from '@mui/material/Chip'
|
||||||
import Dialog from '@mui/material/Dialog'
|
import Dialog from '../components/ModalDialog'
|
||||||
import DialogActions from '@mui/material/DialogActions'
|
import DialogActions from '@mui/material/DialogActions'
|
||||||
import Drawer from '@mui/material/Drawer'
|
import Drawer from '@mui/material/Drawer'
|
||||||
import IconButton from '@mui/material/IconButton'
|
import IconButton from '@mui/material/IconButton'
|
||||||
|
|||||||
Reference in New Issue
Block a user