transfer.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. // Copyright (C) 2019 Nicola Murino
  2. //
  3. // This program is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU Affero General Public License as published
  5. // by the Free Software Foundation, version 3.
  6. //
  7. // This program is distributed in the hope that it will be useful,
  8. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. // GNU Affero General Public License for more details.
  11. //
  12. // You should have received a copy of the GNU Affero General Public License
  13. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  14. package common
  15. import (
  16. "errors"
  17. "fmt"
  18. "io/fs"
  19. "path"
  20. "sync"
  21. "sync/atomic"
  22. "time"
  23. "github.com/drakkan/sftpgo/v2/internal/dataprovider"
  24. "github.com/drakkan/sftpgo/v2/internal/logger"
  25. "github.com/drakkan/sftpgo/v2/internal/metric"
  26. "github.com/drakkan/sftpgo/v2/internal/vfs"
  27. )
  28. var (
  29. // ErrTransferClosed defines the error returned for a closed transfer
  30. ErrTransferClosed = errors.New("transfer already closed")
  31. )
  32. // BaseTransfer contains protocols common transfer details for an upload or a download.
  33. type BaseTransfer struct { //nolint:maligned
  34. ID int64
  35. BytesSent atomic.Int64
  36. BytesReceived atomic.Int64
  37. Fs vfs.Fs
  38. File vfs.File
  39. Connection *BaseConnection
  40. cancelFn func()
  41. fsPath string
  42. effectiveFsPath string
  43. requestPath string
  44. ftpMode string
  45. start time.Time
  46. MaxWriteSize int64
  47. MinWriteOffset int64
  48. InitialSize int64
  49. truncatedSize int64
  50. isNewFile bool
  51. transferType int
  52. AbortTransfer atomic.Bool
  53. aTime time.Time
  54. mTime time.Time
  55. transferQuota dataprovider.TransferQuota
  56. metadata map[string]string
  57. sync.Mutex
  58. errAbort error
  59. ErrTransfer error
  60. }
  61. // NewBaseTransfer returns a new BaseTransfer and adds it to the given connection
  62. func NewBaseTransfer(file vfs.File, conn *BaseConnection, cancelFn func(), fsPath, effectiveFsPath, requestPath string,
  63. transferType int, minWriteOffset, initialSize, maxWriteSize, truncatedSize int64, isNewFile bool, fs vfs.Fs,
  64. transferQuota dataprovider.TransferQuota,
  65. ) *BaseTransfer {
  66. t := &BaseTransfer{
  67. ID: conn.GetTransferID(),
  68. File: file,
  69. Connection: conn,
  70. cancelFn: cancelFn,
  71. fsPath: fsPath,
  72. effectiveFsPath: effectiveFsPath,
  73. start: time.Now(),
  74. transferType: transferType,
  75. MinWriteOffset: minWriteOffset,
  76. InitialSize: initialSize,
  77. isNewFile: isNewFile,
  78. requestPath: requestPath,
  79. MaxWriteSize: maxWriteSize,
  80. truncatedSize: truncatedSize,
  81. transferQuota: transferQuota,
  82. Fs: fs,
  83. }
  84. t.AbortTransfer.Store(false)
  85. t.BytesSent.Store(0)
  86. t.BytesReceived.Store(0)
  87. conn.AddTransfer(t)
  88. return t
  89. }
  90. // GetTransferQuota returns data transfer quota limits
  91. func (t *BaseTransfer) GetTransferQuota() dataprovider.TransferQuota {
  92. return t.transferQuota
  93. }
  94. // SetFtpMode sets the FTP mode for the current transfer
  95. func (t *BaseTransfer) SetFtpMode(mode string) {
  96. t.ftpMode = mode
  97. }
  98. // GetID returns the transfer ID
  99. func (t *BaseTransfer) GetID() int64 {
  100. return t.ID
  101. }
  102. // GetType returns the transfer type
  103. func (t *BaseTransfer) GetType() int {
  104. return t.transferType
  105. }
  106. // GetSize returns the transferred size
  107. func (t *BaseTransfer) GetSize() int64 {
  108. if t.transferType == TransferDownload {
  109. return t.BytesSent.Load()
  110. }
  111. return t.BytesReceived.Load()
  112. }
  113. // GetDownloadedSize returns the transferred size
  114. func (t *BaseTransfer) GetDownloadedSize() int64 {
  115. return t.BytesSent.Load()
  116. }
  117. // GetUploadedSize returns the transferred size
  118. func (t *BaseTransfer) GetUploadedSize() int64 {
  119. return t.BytesReceived.Load()
  120. }
  121. // GetStartTime returns the start time
  122. func (t *BaseTransfer) GetStartTime() time.Time {
  123. return t.start
  124. }
  125. // GetAbortError returns the error to send to the client if the transfer was aborted
  126. func (t *BaseTransfer) GetAbortError() error {
  127. t.Lock()
  128. defer t.Unlock()
  129. if t.errAbort != nil {
  130. return t.errAbort
  131. }
  132. return getQuotaExceededError(t.Connection.protocol)
  133. }
  134. // SignalClose signals that the transfer should be closed after the next read/write.
  135. // The optional error argument allow to send a specific error, otherwise a generic
  136. // transfer aborted error is sent
  137. func (t *BaseTransfer) SignalClose(err error) {
  138. t.Lock()
  139. t.errAbort = err
  140. t.Unlock()
  141. t.AbortTransfer.Store(true)
  142. }
  143. // GetTruncatedSize returns the truncated sized if this is an upload overwriting
  144. // an existing file
  145. func (t *BaseTransfer) GetTruncatedSize() int64 {
  146. return t.truncatedSize
  147. }
  148. // HasSizeLimit returns true if there is an upload or download size limit
  149. func (t *BaseTransfer) HasSizeLimit() bool {
  150. if t.MaxWriteSize > 0 {
  151. return true
  152. }
  153. if t.transferQuota.HasSizeLimits() {
  154. return true
  155. }
  156. return false
  157. }
  158. // GetVirtualPath returns the transfer virtual path
  159. func (t *BaseTransfer) GetVirtualPath() string {
  160. return t.requestPath
  161. }
  162. // GetFsPath returns the transfer filesystem path
  163. func (t *BaseTransfer) GetFsPath() string {
  164. return t.fsPath
  165. }
  166. // SetTimes stores access and modification times if fsPath matches the current file
  167. func (t *BaseTransfer) SetTimes(fsPath string, atime time.Time, mtime time.Time) bool {
  168. if fsPath == t.GetFsPath() {
  169. t.aTime = atime
  170. t.mTime = mtime
  171. return true
  172. }
  173. return false
  174. }
  175. // GetRealFsPath returns the real transfer filesystem path.
  176. // If atomic uploads are enabled this differ from fsPath
  177. func (t *BaseTransfer) GetRealFsPath(fsPath string) string {
  178. if fsPath == t.GetFsPath() {
  179. if t.File != nil || vfs.IsLocalOsFs(t.Fs) {
  180. return t.effectiveFsPath
  181. }
  182. return t.fsPath
  183. }
  184. return ""
  185. }
  186. // SetMetadata sets the metadata for the file
  187. func (t *BaseTransfer) SetMetadata(val map[string]string) {
  188. t.metadata = val
  189. }
  190. // SetCancelFn sets the cancel function for the transfer
  191. func (t *BaseTransfer) SetCancelFn(cancelFn func()) {
  192. t.cancelFn = cancelFn
  193. }
  194. // ConvertError accepts an error that occurs during a read or write and
  195. // converts it into a more understandable form for the client if it is a
  196. // well-known type of error
  197. func (t *BaseTransfer) ConvertError(err error) error {
  198. var pathError *fs.PathError
  199. if errors.As(err, &pathError) {
  200. return fmt.Errorf("%s %s: %s", pathError.Op, t.GetVirtualPath(), pathError.Err.Error())
  201. }
  202. return t.Connection.GetFsError(t.Fs, err)
  203. }
  204. // CheckRead returns an error if read if not allowed
  205. func (t *BaseTransfer) CheckRead() error {
  206. if t.transferQuota.AllowedDLSize == 0 && t.transferQuota.AllowedTotalSize == 0 {
  207. return nil
  208. }
  209. if t.transferQuota.AllowedTotalSize > 0 {
  210. if t.BytesSent.Load()+t.BytesReceived.Load() > t.transferQuota.AllowedTotalSize {
  211. return t.Connection.GetReadQuotaExceededError()
  212. }
  213. } else if t.transferQuota.AllowedDLSize > 0 {
  214. if t.BytesSent.Load() > t.transferQuota.AllowedDLSize {
  215. return t.Connection.GetReadQuotaExceededError()
  216. }
  217. }
  218. return nil
  219. }
  220. // CheckWrite returns an error if write if not allowed
  221. func (t *BaseTransfer) CheckWrite() error {
  222. if t.MaxWriteSize > 0 && t.BytesReceived.Load() > t.MaxWriteSize {
  223. return t.Connection.GetQuotaExceededError()
  224. }
  225. if t.transferQuota.AllowedULSize == 0 && t.transferQuota.AllowedTotalSize == 0 {
  226. return nil
  227. }
  228. if t.transferQuota.AllowedTotalSize > 0 {
  229. if t.BytesSent.Load()+t.BytesReceived.Load() > t.transferQuota.AllowedTotalSize {
  230. return t.Connection.GetQuotaExceededError()
  231. }
  232. } else if t.transferQuota.AllowedULSize > 0 {
  233. if t.BytesReceived.Load() > t.transferQuota.AllowedULSize {
  234. return t.Connection.GetQuotaExceededError()
  235. }
  236. }
  237. return nil
  238. }
  239. // Truncate changes the size of the opened file.
  240. // Supported for local fs only
  241. func (t *BaseTransfer) Truncate(fsPath string, size int64) (int64, error) {
  242. if fsPath == t.GetFsPath() {
  243. if t.File != nil {
  244. initialSize := t.InitialSize
  245. err := t.File.Truncate(size)
  246. if err == nil {
  247. t.Lock()
  248. t.InitialSize = size
  249. if t.MaxWriteSize > 0 {
  250. sizeDiff := initialSize - size
  251. t.MaxWriteSize += sizeDiff
  252. metric.TransferCompleted(t.BytesSent.Load(), t.BytesReceived.Load(),
  253. t.transferType, t.ErrTransfer, vfs.IsSFTPFs(t.Fs))
  254. if t.transferQuota.HasSizeLimits() {
  255. go func(ulSize, dlSize int64, user dataprovider.User) {
  256. dataprovider.UpdateUserTransferQuota(&user, ulSize, dlSize, false) //nolint:errcheck
  257. }(t.BytesReceived.Load(), t.BytesSent.Load(), t.Connection.User)
  258. }
  259. t.BytesReceived.Store(0)
  260. }
  261. t.Unlock()
  262. }
  263. t.Connection.Log(logger.LevelDebug, "file %q truncated to size %v max write size %v new initial size %v err: %v",
  264. fsPath, size, t.MaxWriteSize, t.InitialSize, err)
  265. return initialSize, err
  266. }
  267. if size == 0 && t.BytesSent.Load() == 0 {
  268. // for cloud providers the file is always truncated to zero, we don't support append/resume for uploads.
  269. // For buffered SFTP and local fs we can have buffered bytes so we returns an error
  270. if !vfs.IsBufferedLocalOrSFTPFs(t.Fs) {
  271. return 0, nil
  272. }
  273. }
  274. return 0, vfs.ErrVfsUnsupported
  275. }
  276. return 0, errTransferMismatch
  277. }
  278. // TransferError is called if there is an unexpected error.
  279. // For example network or client issues
  280. func (t *BaseTransfer) TransferError(err error) {
  281. t.Lock()
  282. defer t.Unlock()
  283. if t.ErrTransfer != nil {
  284. return
  285. }
  286. t.ErrTransfer = err
  287. if t.cancelFn != nil {
  288. t.cancelFn()
  289. }
  290. elapsed := time.Since(t.start).Nanoseconds() / 1000000
  291. t.Connection.Log(logger.LevelError, "Unexpected error for transfer, path: %q, error: \"%v\" bytes sent: %v, "+
  292. "bytes received: %v transfer running since %v ms", t.fsPath, t.ErrTransfer, t.BytesSent.Load(),
  293. t.BytesReceived.Load(), elapsed)
  294. }
  295. func (t *BaseTransfer) getUploadFileSize() (int64, int, error) {
  296. var fileSize int64
  297. var deletedFiles int
  298. info, err := t.Fs.Stat(t.fsPath)
  299. if err == nil {
  300. fileSize = info.Size()
  301. }
  302. if t.ErrTransfer != nil && vfs.IsCryptOsFs(t.Fs) {
  303. errDelete := t.Fs.Remove(t.fsPath, false)
  304. if errDelete != nil {
  305. t.Connection.Log(logger.LevelWarn, "error removing partial crypto file %q: %v", t.fsPath, errDelete)
  306. } else {
  307. fileSize = 0
  308. deletedFiles = 1
  309. t.BytesReceived.Store(0)
  310. t.MinWriteOffset = 0
  311. }
  312. }
  313. return fileSize, deletedFiles, err
  314. }
  315. // return 1 if the file is outside the user home dir
  316. func (t *BaseTransfer) checkUploadOutsideHomeDir(err error) int {
  317. if err == nil {
  318. return 0
  319. }
  320. if Config.TempPath == "" {
  321. return 0
  322. }
  323. err = t.Fs.Remove(t.effectiveFsPath, false)
  324. t.Connection.Log(logger.LevelWarn, "upload in temp path cannot be renamed, delete temporary file: %q, deletion error: %v",
  325. t.effectiveFsPath, err)
  326. // the file is outside the home dir so don't update the quota
  327. t.BytesReceived.Store(0)
  328. t.MinWriteOffset = 0
  329. return 1
  330. }
  331. // Close it is called when the transfer is completed.
  332. // It logs the transfer info, updates the user quota (for uploads)
  333. // and executes any defined action.
  334. // If there is an error no action will be executed and, in atomic mode,
  335. // we try to delete the temporary file
  336. func (t *BaseTransfer) Close() error {
  337. defer t.Connection.RemoveTransfer(t)
  338. var err error
  339. numFiles := t.getUploadedFiles()
  340. metric.TransferCompleted(t.BytesSent.Load(), t.BytesReceived.Load(),
  341. t.transferType, t.ErrTransfer, vfs.IsSFTPFs(t.Fs))
  342. if t.transferQuota.HasSizeLimits() {
  343. dataprovider.UpdateUserTransferQuota(&t.Connection.User, t.BytesReceived.Load(), //nolint:errcheck
  344. t.BytesSent.Load(), false)
  345. }
  346. if (t.File != nil || vfs.IsLocalOsFs(t.Fs)) && t.Connection.IsQuotaExceededError(t.ErrTransfer) {
  347. // if quota is exceeded we try to remove the partial file for uploads to local filesystem
  348. err = t.Fs.Remove(t.effectiveFsPath, false)
  349. if err == nil {
  350. t.BytesReceived.Store(0)
  351. t.MinWriteOffset = 0
  352. }
  353. t.Connection.Log(logger.LevelWarn, "upload denied due to space limit, delete temporary file: %q, deletion error: %v",
  354. t.effectiveFsPath, err)
  355. } else if t.isAtomicUpload() {
  356. if t.ErrTransfer == nil || Config.UploadMode&UploadModeAtomicWithResume != 0 {
  357. _, _, err = t.Fs.Rename(t.effectiveFsPath, t.fsPath)
  358. t.Connection.Log(logger.LevelDebug, "atomic upload completed, rename: %q -> %q, error: %v",
  359. t.effectiveFsPath, t.fsPath, err)
  360. // the file must be removed if it is uploaded to a path outside the home dir and cannot be renamed
  361. t.checkUploadOutsideHomeDir(err)
  362. } else {
  363. err = t.Fs.Remove(t.effectiveFsPath, false)
  364. t.Connection.Log(logger.LevelWarn, "atomic upload completed with error: \"%v\", delete temporary file: %q, deletion error: %v",
  365. t.ErrTransfer, t.effectiveFsPath, err)
  366. if err == nil {
  367. t.BytesReceived.Store(0)
  368. t.MinWriteOffset = 0
  369. }
  370. }
  371. }
  372. elapsed := time.Since(t.start).Nanoseconds() / 1000000
  373. var uploadFileSize int64
  374. if t.transferType == TransferDownload {
  375. logger.TransferLog(downloadLogSender, t.fsPath, elapsed, t.BytesSent.Load(), t.Connection.User.Username,
  376. t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
  377. ExecuteActionNotification(t.Connection, operationDownload, t.fsPath, t.requestPath, "", "", "", //nolint:errcheck
  378. t.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)
  379. } else {
  380. statSize, deletedFiles, errStat := t.getUploadFileSize()
  381. if errStat == nil {
  382. uploadFileSize = statSize
  383. } else {
  384. uploadFileSize = t.BytesReceived.Load() + t.MinWriteOffset
  385. if t.Fs.IsNotExist(errStat) {
  386. uploadFileSize = 0
  387. numFiles--
  388. }
  389. }
  390. numFiles -= deletedFiles
  391. t.Connection.Log(logger.LevelDebug, "upload file size %d, num files %d, deleted files %d, fs path %q",
  392. uploadFileSize, numFiles, deletedFiles, t.fsPath)
  393. numFiles, uploadFileSize = t.executeUploadHook(numFiles, uploadFileSize, elapsed)
  394. t.updateQuota(numFiles, uploadFileSize)
  395. t.updateTimes()
  396. logger.TransferLog(uploadLogSender, t.fsPath, elapsed, t.BytesReceived.Load(), t.Connection.User.Username,
  397. t.Connection.ID, t.Connection.protocol, t.Connection.localAddr, t.Connection.remoteAddr, t.ftpMode)
  398. }
  399. if t.ErrTransfer != nil {
  400. t.Connection.Log(logger.LevelError, "transfer error: %v, path: %q", t.ErrTransfer, t.fsPath)
  401. if err == nil {
  402. err = t.ErrTransfer
  403. }
  404. }
  405. t.updateTransferTimestamps(uploadFileSize, elapsed)
  406. return err
  407. }
  408. func (t *BaseTransfer) isAtomicUpload() bool {
  409. return t.transferType == TransferUpload && t.effectiveFsPath != t.fsPath
  410. }
  411. func (t *BaseTransfer) updateTransferTimestamps(uploadFileSize, elapsed int64) {
  412. if t.ErrTransfer != nil {
  413. return
  414. }
  415. if t.transferType == TransferUpload {
  416. if t.Connection.User.FirstUpload == 0 && !t.Connection.uploadDone.Load() {
  417. if err := dataprovider.UpdateUserTransferTimestamps(t.Connection.User.Username, true); err == nil {
  418. t.Connection.uploadDone.Store(true)
  419. ExecuteActionNotification(t.Connection, operationFirstUpload, t.fsPath, t.requestPath, "", //nolint:errcheck
  420. "", "", uploadFileSize, t.ErrTransfer, elapsed, t.metadata)
  421. }
  422. }
  423. return
  424. }
  425. if t.Connection.User.FirstDownload == 0 && !t.Connection.downloadDone.Load() && t.BytesSent.Load() > 0 {
  426. if err := dataprovider.UpdateUserTransferTimestamps(t.Connection.User.Username, false); err == nil {
  427. t.Connection.downloadDone.Store(true)
  428. ExecuteActionNotification(t.Connection, operationFirstDownload, t.fsPath, t.requestPath, "", //nolint:errcheck
  429. "", "", t.BytesSent.Load(), t.ErrTransfer, elapsed, t.metadata)
  430. }
  431. }
  432. }
  433. func (t *BaseTransfer) executeUploadHook(numFiles int, fileSize, elapsed int64) (int, int64) {
  434. err := ExecuteActionNotification(t.Connection, operationUpload, t.fsPath, t.requestPath, "", "", "",
  435. fileSize, t.ErrTransfer, elapsed, t.metadata)
  436. if err != nil {
  437. if t.ErrTransfer == nil {
  438. t.ErrTransfer = err
  439. }
  440. // try to remove the uploaded file
  441. err = t.Fs.Remove(t.fsPath, false)
  442. if err == nil {
  443. numFiles--
  444. fileSize = 0
  445. t.BytesReceived.Store(0)
  446. t.MinWriteOffset = 0
  447. } else {
  448. t.Connection.Log(logger.LevelWarn, "unable to remove path %q after upload hook failure: %v", t.fsPath, err)
  449. }
  450. }
  451. return numFiles, fileSize
  452. }
  453. func (t *BaseTransfer) getUploadedFiles() int {
  454. numFiles := 0
  455. if t.isNewFile {
  456. numFiles = 1
  457. }
  458. return numFiles
  459. }
  460. func (t *BaseTransfer) updateTimes() {
  461. if !t.aTime.IsZero() && !t.mTime.IsZero() {
  462. err := t.Fs.Chtimes(t.fsPath, t.aTime, t.mTime, false)
  463. t.Connection.Log(logger.LevelDebug, "set times for file %q, atime: %v, mtime: %v, err: %v",
  464. t.fsPath, t.aTime, t.mTime, err)
  465. }
  466. }
  467. func (t *BaseTransfer) updateQuota(numFiles int, fileSize int64) bool {
  468. // Uploads on some filesystem (S3 and similar) are atomic, if there is an error nothing is uploaded
  469. if t.File == nil && t.ErrTransfer != nil && vfs.HasImplicitAtomicUploads(t.Fs) {
  470. return false
  471. }
  472. sizeDiff := fileSize - t.InitialSize
  473. if t.transferType == TransferUpload && (numFiles != 0 || sizeDiff != 0) {
  474. vfolder, err := t.Connection.User.GetVirtualFolderForPath(path.Dir(t.requestPath))
  475. if err == nil {
  476. dataprovider.UpdateVirtualFolderQuota(&vfolder.BaseVirtualFolder, numFiles, //nolint:errcheck
  477. sizeDiff, false)
  478. if vfolder.IsIncludedInUserQuota() {
  479. dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
  480. }
  481. } else {
  482. dataprovider.UpdateUserQuota(&t.Connection.User, numFiles, sizeDiff, false) //nolint:errcheck
  483. }
  484. return true
  485. }
  486. return false
  487. }
  488. // HandleThrottle manage bandwidth throttling
  489. func (t *BaseTransfer) HandleThrottle() {
  490. var wantedBandwidth int64
  491. var trasferredBytes int64
  492. if t.transferType == TransferDownload {
  493. wantedBandwidth = t.Connection.User.DownloadBandwidth
  494. trasferredBytes = t.BytesSent.Load()
  495. } else {
  496. wantedBandwidth = t.Connection.User.UploadBandwidth
  497. trasferredBytes = t.BytesReceived.Load()
  498. }
  499. if wantedBandwidth > 0 {
  500. // real and wanted elapsed as milliseconds, bytes as kilobytes
  501. realElapsed := time.Since(t.start).Nanoseconds() / 1000000
  502. // trasferredBytes / 1024 = KB/s, we multiply for 1000 to get milliseconds
  503. wantedElapsed := 1000 * (trasferredBytes / 1024) / wantedBandwidth
  504. if wantedElapsed > realElapsed {
  505. toSleep := time.Duration(wantedElapsed - realElapsed)
  506. time.Sleep(toSleep * time.Millisecond)
  507. }
  508. }
  509. }