mirror of
https://github.com/Eugeny/tabby.git
synced 2025-06-08 05:20:01 +00:00
277 lines
9.5 KiB
TypeScript
277 lines
9.5 KiB
TypeScript
import * as C from 'constants'
|
|
import { posix as path } from 'path'
|
|
import { Component, Input, Output, EventEmitter, Inject, Optional } from '@angular/core'
|
|
import { FileUpload, DirectoryUpload, MenuItemOptions, NotificationsService, PlatformService } from 'tabby-core'
|
|
import { SFTPSession, SFTPFile } from '../session/sftp'
|
|
import { SSHSession } from '../session/ssh'
|
|
import { SFTPContextMenuItemProvider } from '../api'
|
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
|
import { SFTPCreateDirectoryModalComponent } from './sftpCreateDirectoryModal.component'
|
|
|
|
interface PathSegment {
|
|
name: string
|
|
path: string
|
|
}
|
|
|
|
@Component({
|
|
selector: 'sftp-panel',
|
|
templateUrl: './sftpPanel.component.pug',
|
|
styleUrls: ['./sftpPanel.component.scss'],
|
|
})
|
|
export class SFTPPanelComponent {
|
|
@Input() session: SSHSession
|
|
@Output() closed = new EventEmitter<void>()
|
|
sftp: SFTPSession
|
|
fileList: SFTPFile[]|null = null
|
|
@Input() path = '/'
|
|
@Output() pathChange = new EventEmitter<string>()
|
|
pathSegments: PathSegment[] = []
|
|
@Input() cwdDetectionAvailable = false
|
|
editingPath: string|null = null
|
|
|
|
constructor (
|
|
private ngbModal: NgbModal,
|
|
private notifications: NotificationsService,
|
|
public platform: PlatformService,
|
|
@Optional() @Inject(SFTPContextMenuItemProvider) protected contextMenuProviders: SFTPContextMenuItemProvider[],
|
|
) {
|
|
this.contextMenuProviders.sort((a, b) => a.weight - b.weight)
|
|
}
|
|
|
|
async ngOnInit (): Promise<void> {
|
|
this.sftp = await this.session.openSFTP()
|
|
try {
|
|
await this.navigate(this.path)
|
|
} catch (error) {
|
|
console.warn('Could not navigate to', this.path, ':', error)
|
|
this.notifications.error(error.message)
|
|
await this.navigate('/')
|
|
}
|
|
}
|
|
|
|
async navigate (newPath: string, fallbackOnError = true): Promise<void> {
|
|
const previousPath = this.path
|
|
this.path = newPath
|
|
this.pathChange.next(this.path)
|
|
|
|
let p = newPath
|
|
this.pathSegments = []
|
|
while (p !== '/') {
|
|
this.pathSegments.unshift({
|
|
name: path.basename(p),
|
|
path: p,
|
|
})
|
|
p = path.dirname(p)
|
|
}
|
|
|
|
this.fileList = null
|
|
try {
|
|
this.fileList = await this.sftp.readdir(this.path)
|
|
} catch (error) {
|
|
this.notifications.error(error.message)
|
|
if (previousPath && fallbackOnError) {
|
|
this.navigate(previousPath, false)
|
|
}
|
|
return
|
|
}
|
|
|
|
const dirKey = a => a.isDirectory ? 1 : 0
|
|
this.fileList.sort((a, b) =>
|
|
dirKey(b) - dirKey(a) ||
|
|
a.name.localeCompare(b.name))
|
|
}
|
|
|
|
getFileType (fileExtension: string): string {
|
|
const codeExtensions = ['js', 'ts', 'py', 'java', 'cpp', 'h', 'cs', 'html', 'css', 'rb', 'php', 'swift', 'go', 'kt', 'sh', 'json', 'cc', 'c', 'xml']
|
|
const imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp']
|
|
const pdfExtensions = ['pdf']
|
|
const archiveExtensions = ['zip', 'rar', 'tar', 'gz']
|
|
const wordExtensions = ['doc', 'docx']
|
|
const videoExtensions = ['mp4', 'avi', 'mkv', 'mov']
|
|
const powerpointExtensions = ['ppt', 'pptx']
|
|
const textExtensions = ['txt', 'log']
|
|
const audioExtensions = ['mp3', 'wav', 'flac']
|
|
const excelExtensions = ['xls', 'xlsx']
|
|
|
|
const lowerCaseExtension = fileExtension.toLowerCase()
|
|
|
|
if (codeExtensions.includes(lowerCaseExtension)) {
|
|
return 'code'
|
|
} else if (imageExtensions.includes(lowerCaseExtension)) {
|
|
return 'image'
|
|
} else if (pdfExtensions.includes(lowerCaseExtension)) {
|
|
return 'pdf'
|
|
} else if (archiveExtensions.includes(lowerCaseExtension)) {
|
|
return 'archive'
|
|
} else if (wordExtensions.includes(lowerCaseExtension)) {
|
|
return 'word'
|
|
} else if (videoExtensions.includes(lowerCaseExtension)) {
|
|
return 'video'
|
|
} else if (powerpointExtensions.includes(lowerCaseExtension)) {
|
|
return 'powerpoint'
|
|
} else if (textExtensions.includes(lowerCaseExtension)) {
|
|
return 'text'
|
|
} else if (audioExtensions.includes(lowerCaseExtension)) {
|
|
return 'audio'
|
|
} else if (excelExtensions.includes(lowerCaseExtension)) {
|
|
return 'excel'
|
|
} else {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
getIcon (item: SFTPFile): string {
|
|
if (item.isDirectory) {
|
|
return 'fas fa-folder text-info'
|
|
}
|
|
if (item.isSymlink) {
|
|
return 'fas fa-link text-warning'
|
|
}
|
|
const fileMatch = /\.([^.]+)$/.exec(item.name)
|
|
const extension = fileMatch ? fileMatch[1] : null
|
|
if (extension !== null) {
|
|
const fileType = this.getFileType(extension)
|
|
|
|
switch (fileType) {
|
|
case 'unknown':
|
|
return 'fas fa-file'
|
|
default:
|
|
return `fa-solid fa-file-${fileType} `
|
|
}
|
|
}
|
|
return 'fas fa-file'
|
|
}
|
|
|
|
goUp (): void {
|
|
this.navigate(path.dirname(this.path))
|
|
}
|
|
|
|
async open (item: SFTPFile): Promise<void> {
|
|
if (item.isDirectory) {
|
|
await this.navigate(item.fullPath)
|
|
} else if (item.isSymlink) {
|
|
const target = path.resolve(this.path, await this.sftp.readlink(item.fullPath))
|
|
const stat = await this.sftp.stat(target)
|
|
if (stat.isDirectory) {
|
|
await this.navigate(item.fullPath)
|
|
} else {
|
|
await this.download(item.fullPath, stat.mode, stat.size)
|
|
}
|
|
} else {
|
|
await this.download(item.fullPath, item.mode, item.size)
|
|
}
|
|
}
|
|
|
|
async openCreateDirectoryModal (): Promise<void> {
|
|
const modal = this.ngbModal.open(SFTPCreateDirectoryModalComponent)
|
|
const directoryName = await modal.result.catch(() => null)
|
|
if (directoryName?.trim()) {
|
|
this.sftp.mkdir(path.join(this.path, directoryName)).then(() => {
|
|
this.notifications.notice('The directory was created successfully')
|
|
this.navigate(path.join(this.path, directoryName))
|
|
}).catch(() => {
|
|
this.notifications.error('The directory could not be created')
|
|
})
|
|
}
|
|
}
|
|
|
|
async upload (): Promise<void> {
|
|
const transfers = await this.platform.startUpload({ multiple: true })
|
|
await Promise.all(transfers.map(t => this.uploadOne(t)))
|
|
}
|
|
|
|
async uploadFolder (): Promise<void> {
|
|
const transfer = await this.platform.startUploadDirectory()
|
|
await this.uploadOneFolder(transfer)
|
|
}
|
|
|
|
async uploadOneFolder (transfer: DirectoryUpload, accumPath = ''): Promise<void> {
|
|
const savedPath = this.path
|
|
for(const t of transfer.getChildrens()) {
|
|
if (t instanceof DirectoryUpload) {
|
|
try {
|
|
await this.sftp.mkdir(path.posix.join(this.path, accumPath, t.getName()))
|
|
} catch {
|
|
// Intentionally ignoring errors from making duplicate dirs.
|
|
}
|
|
await this.uploadOneFolder(t, path.posix.join(accumPath, t.getName()))
|
|
} else {
|
|
await this.sftp.upload(path.posix.join(this.path, accumPath, t.getName()), t)
|
|
}
|
|
}
|
|
if (this.path === savedPath) {
|
|
await this.navigate(this.path)
|
|
}
|
|
}
|
|
|
|
async uploadOne (transfer: FileUpload): Promise<void> {
|
|
const savedPath = this.path
|
|
await this.sftp.upload(path.join(this.path, transfer.getName()), transfer)
|
|
if (this.path === savedPath) {
|
|
await this.navigate(this.path)
|
|
}
|
|
}
|
|
|
|
async download (itemPath: string, mode: number, size: number): Promise<void> {
|
|
const transfer = await this.platform.startDownload(path.basename(itemPath), mode, size)
|
|
if (!transfer) {
|
|
return
|
|
}
|
|
this.sftp.download(itemPath, transfer)
|
|
}
|
|
|
|
getModeString (item: SFTPFile): string {
|
|
const s = 'SGdrwxrwxrwx'
|
|
const e = ' ---------'
|
|
const c = [
|
|
0o4000, 0o2000, C.S_IFDIR,
|
|
C.S_IRUSR, C.S_IWUSR, C.S_IXUSR,
|
|
C.S_IRGRP, C.S_IWGRP, C.S_IXGRP,
|
|
C.S_IROTH, C.S_IWOTH, C.S_IXOTH,
|
|
]
|
|
let result = ''
|
|
for (let i = 0; i < c.length; i++) {
|
|
result += item.mode & c[i] ? s[i] : e[i]
|
|
}
|
|
return result
|
|
}
|
|
|
|
async buildContextMenu (item: SFTPFile): Promise<MenuItemOptions[]> {
|
|
let items: MenuItemOptions[] = []
|
|
for (const section of await Promise.all(this.contextMenuProviders.map(x => x.getItems(item, this)))) {
|
|
items.push({ type: 'separator' })
|
|
items = items.concat(section)
|
|
}
|
|
return items.slice(1)
|
|
}
|
|
|
|
async showContextMenu (item: SFTPFile, event: MouseEvent): Promise<void> {
|
|
event.preventDefault()
|
|
this.platform.popupContextMenu(await this.buildContextMenu(item), event)
|
|
}
|
|
|
|
get shouldShowCWDTip (): boolean {
|
|
return !window.localStorage.sshCWDTipDismissed
|
|
}
|
|
|
|
dismissCWDTip (): void {
|
|
window.localStorage.sshCWDTipDismissed = 'true'
|
|
}
|
|
|
|
editPath (): void {
|
|
this.editingPath = this.path
|
|
}
|
|
|
|
confirmPath (): void {
|
|
if (this.editingPath === null) {
|
|
return
|
|
}
|
|
this.navigate(this.editingPath)
|
|
this.editingPath = null
|
|
}
|
|
|
|
close (): void {
|
|
this.closed.emit()
|
|
}
|
|
}
|