Compare commits
1 Commits
main
...
fix/sftp-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
55e7aa0a87 |
@@ -257,6 +257,8 @@ class SshService implements ConnectComms {
|
||||
('cleanup', 'Finalize'),
|
||||
('logs', 'Fetch logs'),
|
||||
]);
|
||||
final staging =
|
||||
'/tmp/peachdeploy-${DateTime.now().millisecondsSinceEpoch}.jar';
|
||||
try {
|
||||
steps.active('connect');
|
||||
await _connect(server);
|
||||
@@ -265,8 +267,6 @@ class SshService implements ConnectComms {
|
||||
final target = service.remotePath(jarLabel);
|
||||
final backup = '$target.bak';
|
||||
final dir = target.substring(0, target.lastIndexOf('/'));
|
||||
final staging =
|
||||
'/tmp/peachdeploy-${DateTime.now().millisecondsSinceEpoch}.jar';
|
||||
|
||||
await _run('mkdir -p ${_shQuote(dir)}', sudo: sudo, password: pw);
|
||||
|
||||
@@ -415,6 +415,8 @@ class SshService implements ConnectComms {
|
||||
}
|
||||
_log(LogLevel.error, 'Deploy failed: $e');
|
||||
return DeployResult(false, _canRevert, 'Error: $e');
|
||||
} finally {
|
||||
await _cleanStaging(server, [staging]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -632,6 +634,8 @@ class SshService implements ConnectComms {
|
||||
}
|
||||
_log(LogLevel.error, 'Web deploy failed: $e');
|
||||
return DeployResult(false, _canRevert, 'Error: $e');
|
||||
} finally {
|
||||
await _cleanStaging(server, [archive, staging]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -923,6 +927,8 @@ class SshService implements ConnectComms {
|
||||
steps.failRemaining();
|
||||
_log(LogLevel.error, 'Unit install failed: $e');
|
||||
return DeployResult(false, false, 'Error: $e');
|
||||
} finally {
|
||||
await _cleanStaging(server, [tmp]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2170,27 +2176,84 @@ class SshService implements ConnectComms {
|
||||
final total = bytes.length;
|
||||
var sent = 0;
|
||||
const chunk = 32 * 1024;
|
||||
// Chunks in flight per batch (~1 MB) — keeps the pipe full without
|
||||
// dartssh2's SftpFileWriter, which leaks write errors as unhandled
|
||||
// exceptions and never completes its future when the server rejects data.
|
||||
const window = 32;
|
||||
final meter = _SpeedMeter();
|
||||
final stream =
|
||||
Stream<Uint8List>.fromIterable([
|
||||
for (var i = 0; i < total; i += chunk)
|
||||
Uint8List.sublistView(
|
||||
bytes,
|
||||
i,
|
||||
i + chunk > total ? total : i + chunk,
|
||||
),
|
||||
]).map((c) {
|
||||
sent += c.length;
|
||||
try {
|
||||
for (var i = 0; i < total; i += chunk * window) {
|
||||
final batch = <Future<void>>[];
|
||||
for (var j = i; j < total && j < i + chunk * window; j += chunk) {
|
||||
final end = j + chunk > total ? total : j + chunk;
|
||||
batch.add(
|
||||
file.writeBytes(Uint8List.sublistView(bytes, j, end), offset: j),
|
||||
);
|
||||
}
|
||||
await Future.wait(batch);
|
||||
sent = (i + chunk * window) > total ? total : i + chunk * window;
|
||||
if (meter.tick(sent)) {
|
||||
_emitTransfer(sent, total, meter, uploading: true);
|
||||
}
|
||||
return c;
|
||||
});
|
||||
await file.write(stream);
|
||||
}
|
||||
} on SftpStatusError catch (e) {
|
||||
try {
|
||||
await file.close();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await sftp.remove(remotePath);
|
||||
} catch (_) {}
|
||||
throw Exception(
|
||||
'SFTP write to $remotePath failed (${e.message}, code ${e.code})'
|
||||
'${await _freeSpaceHint(sftp, remotePath, total)}',
|
||||
);
|
||||
}
|
||||
await file.close();
|
||||
_emitTransfer(total, total, meter, uploading: true, force: true);
|
||||
}
|
||||
|
||||
/// Removes this run's /tmp staging files (no-op if they were already moved
|
||||
/// into place) plus stale ones left by earlier crashed or cancelled runs.
|
||||
/// Best-effort: never throws, so it can't mask the deploy's own result.
|
||||
Future<void> _cleanStaging(ServerProfile s, List<String> paths) async {
|
||||
try {
|
||||
await _connect(s).timeout(const Duration(seconds: 20));
|
||||
await _run(
|
||||
'rm -rf ${paths.map(_shQuote).join(' ')}; '
|
||||
"find /tmp -maxdepth 1 \\( -name 'peachdeploy-*' -o -name 'peachweb-*' \\) "
|
||||
'-mmin +60 -exec rm -rf {} + 2>/dev/null; true',
|
||||
sudo: s.useSudo,
|
||||
password: s.password,
|
||||
).timeout(const Duration(seconds: 30));
|
||||
} catch (e) {
|
||||
_log(LogLevel.warn, 'Could not clean staging files: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Best-effort explanation for a generic SFTP write failure — usually the
|
||||
/// target filesystem (often /tmp) is full or over quota.
|
||||
Future<String> _freeSpaceHint(
|
||||
SftpClient sftp,
|
||||
String remotePath,
|
||||
int needed,
|
||||
) async {
|
||||
try {
|
||||
final slash = remotePath.lastIndexOf('/');
|
||||
final dir = slash > 0 ? remotePath.substring(0, slash) : '/';
|
||||
final vfs = await sftp.statvfs(dir);
|
||||
if (vfs.isReadOnly) return ' — $dir is mounted read-only.';
|
||||
final unit = vfs.fundamentalBlockSize > 0
|
||||
? vfs.fundamentalBlockSize
|
||||
: vfs.blockSize;
|
||||
final free = unit * vfs.freeBlocksForNonRoot;
|
||||
return ' — $dir has ${_fmtBytes(free)} free, upload needs '
|
||||
'${_fmtBytes(needed)}. Free up space on the server.';
|
||||
} catch (_) {
|
||||
return ' — the remote filesystem rejected the data; check free space '
|
||||
'and quota on the server (df -h).';
|
||||
}
|
||||
}
|
||||
|
||||
// ---- lifecycle ------------------------------------------------------------
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user