sftp.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import * as C from 'constants'
  2. import { Subject, Observable } from 'rxjs'
  3. import { posix as posixPath } from 'path'
  4. import { Injector, NgZone } from '@angular/core'
  5. import { FileDownload, FileUpload, Logger, LogService, wrapPromise } from 'tabby-core'
  6. import { SFTPWrapper } from 'ssh2'
  7. import { promisify } from 'util'
  8. import type { FileEntry, Stats } from 'ssh2-streams'
  9. export interface SFTPFile {
  10. name: string
  11. fullPath: string
  12. isDirectory: boolean
  13. isSymlink: boolean
  14. mode: number
  15. size: number
  16. modified: Date
  17. }
  18. export class SFTPFileHandle {
  19. position = 0
  20. constructor (
  21. private sftp: SFTPWrapper,
  22. private handle: Buffer,
  23. private zone: NgZone,
  24. ) { }
  25. read (): Promise<Buffer> {
  26. const buffer = Buffer.alloc(256 * 1024)
  27. return wrapPromise(this.zone, new Promise((resolve, reject) => {
  28. while (true) {
  29. const wait = this.sftp.read(this.handle, buffer, 0, buffer.length, this.position, (err, read) => {
  30. if (err) {
  31. reject(err)
  32. return
  33. }
  34. this.position += read
  35. resolve(buffer.slice(0, read))
  36. })
  37. if (!wait) {
  38. break
  39. }
  40. }
  41. }))
  42. }
  43. write (chunk: Buffer): Promise<void> {
  44. return wrapPromise(this.zone, new Promise<void>((resolve, reject) => {
  45. while (true) {
  46. const wait = this.sftp.write(this.handle, chunk, 0, chunk.length, this.position, err => {
  47. if (err) {
  48. reject(err)
  49. return
  50. }
  51. this.position += chunk.length
  52. resolve()
  53. })
  54. if (!wait) {
  55. break
  56. }
  57. }
  58. }))
  59. }
  60. close (): Promise<void> {
  61. return wrapPromise(this.zone, promisify(this.sftp.close.bind(this.sftp))(this.handle))
  62. }
  63. }
  64. export class SFTPSession {
  65. get closed$ (): Observable<void> { return this.closed }
  66. private closed = new Subject<void>()
  67. private zone: NgZone
  68. private logger: Logger
  69. constructor (private sftp: SFTPWrapper, injector: Injector) {
  70. this.zone = injector.get(NgZone)
  71. this.logger = injector.get(LogService).create('sftp')
  72. sftp.on('close', () => {
  73. this.closed.next()
  74. this.closed.complete()
  75. })
  76. }
  77. async readdir (p: string): Promise<SFTPFile[]> {
  78. this.logger.debug('readdir', p)
  79. const entries = await wrapPromise(this.zone, promisify<FileEntry[]>(f => this.sftp.readdir(p, f))())
  80. return entries.map(entry => this._makeFile(
  81. posixPath.join(p, entry.filename), entry,
  82. ))
  83. }
  84. readlink (p: string): Promise<string> {
  85. this.logger.debug('readlink', p)
  86. return wrapPromise(this.zone, promisify<string>(f => this.sftp.readlink(p, f))())
  87. }
  88. async stat (p: string): Promise<SFTPFile> {
  89. this.logger.debug('stat', p)
  90. const stats = await wrapPromise(this.zone, promisify<Stats>(f => this.sftp.stat(p, f))())
  91. return {
  92. name: posixPath.basename(p),
  93. fullPath: p,
  94. isDirectory: stats.isDirectory(),
  95. isSymlink: stats.isSymbolicLink(),
  96. mode: stats.mode,
  97. size: stats.size,
  98. modified: new Date(stats.mtime * 1000),
  99. }
  100. }
  101. async open (p: string, mode: string): Promise<SFTPFileHandle> {
  102. this.logger.debug('open', p)
  103. const handle = await wrapPromise(this.zone, promisify<Buffer>(f => this.sftp.open(p, mode, f))())
  104. return new SFTPFileHandle(this.sftp, handle, this.zone)
  105. }
  106. async rmdir (p: string): Promise<void> {
  107. this.logger.debug('rmdir', p)
  108. await promisify((f: any) => this.sftp.rmdir(p, f))()
  109. }
  110. async mkdir (p: string): Promise<void> {
  111. this.logger.debug('mkdir', p)
  112. await promisify((f: any) => this.sftp.mkdir(p, f))()
  113. }
  114. async rename (oldPath: string, newPath: string): Promise<void> {
  115. this.logger.debug('rename', oldPath, newPath)
  116. await promisify((f: any) => this.sftp.rename(oldPath, newPath, f))()
  117. }
  118. async unlink (p: string): Promise<void> {
  119. this.logger.debug('unlink', p)
  120. await promisify((f: any) => this.sftp.unlink(p, f))()
  121. }
  122. async chmod (p: string, mode: string|number): Promise<void> {
  123. this.logger.debug('chmod', p, mode)
  124. await promisify((f: any) => this.sftp.chmod(p, mode, f))()
  125. }
  126. async upload (path: string, transfer: FileUpload): Promise<void> {
  127. this.logger.info('Uploading into', path)
  128. const tempPath = path + '.tabby-upload'
  129. try {
  130. const handle = await this.open(tempPath, 'w')
  131. while (true) {
  132. const chunk = await transfer.read()
  133. if (!chunk.length) {
  134. break
  135. }
  136. await handle.write(chunk)
  137. }
  138. handle.close()
  139. try {
  140. await this.unlink(path)
  141. } catch { }
  142. await this.rename(tempPath, path)
  143. transfer.close()
  144. } catch (e) {
  145. transfer.cancel()
  146. this.unlink(tempPath)
  147. throw e
  148. }
  149. }
  150. async download (path: string, transfer: FileDownload): Promise<void> {
  151. this.logger.info('Downloading', path)
  152. try {
  153. const handle = await this.open(path, 'r')
  154. while (true) {
  155. const chunk = await handle.read()
  156. if (!chunk.length) {
  157. break
  158. }
  159. await transfer.write(chunk)
  160. }
  161. transfer.close()
  162. handle.close()
  163. } catch (e) {
  164. transfer.cancel()
  165. throw e
  166. }
  167. }
  168. private _makeFile (p: string, entry: FileEntry): SFTPFile {
  169. return {
  170. fullPath: p,
  171. name: posixPath.basename(p),
  172. isDirectory: (entry.attrs.mode & C.S_IFDIR) === C.S_IFDIR,
  173. isSymlink: (entry.attrs.mode & C.S_IFLNK) === C.S_IFLNK,
  174. mode: entry.attrs.mode,
  175. size: entry.attrs.size,
  176. modified: new Date(entry.attrs.mtime * 1000),
  177. }
  178. }
  179. }