http downloading in chunks

This commit is contained in:
2025-05-24 00:55:56 -05:00
parent 1248b94244
commit 23813a4c31
5 changed files with 811 additions and 29 deletions

View File

@@ -0,0 +1,234 @@
// HTTP Download Actor
// Handles download requests and progress queries
var http = use('http');
var os = use('os');
// Actor state
var state = {
downloading: false,
current_url: null,
total_bytes: 0,
downloaded_bytes: 0,
start_time: 0,
error: null,
connection: null,
download_msg: null,
chunks: []
};
// Helper to calculate progress percentage
function get_progress() {
if (state.total_bytes === 0) {
return 0;
}
return Math.round((state.downloaded_bytes / state.total_bytes) * 100);
}
// Helper to format status response
function get_status() {
if (!state.downloading) {
return {
status: 'idle',
error: state.error
};
}
var elapsed = os.now() - state.start_time;
var bytes_per_sec = elapsed > 0 ? state.downloaded_bytes / elapsed : 0;
return {
status: 'downloading',
url: state.current_url,
progress: get_progress(),
downloaded_bytes: state.downloaded_bytes,
total_bytes: state.total_bytes,
elapsed_seconds: elapsed,
bytes_per_second: Math.round(bytes_per_sec)
};
}
// Main message receiver
$_.receiver(function(msg) {
switch (msg.type) {
case 'download':
if (state.downloading) {
send(msg, {
type: 'error',
error: 'Already downloading',
current_url: state.current_url
});
return;
}
if (!msg.url) {
send(msg, {
type: 'error',
error: 'No URL provided'
});
return;
}
// Start download
state.downloading = true;
state.current_url = msg.url;
state.total_bytes = 0;
state.downloaded_bytes = 0;
state.start_time = os.now();
state.error = null;
state.download_msg = msg;
state.chunks = [];
try {
// Start the connection
state.connection = http.fetch_start(msg.url, msg.options || {});
if (!state.connection) {
throw new Error('Failed to start download');
}
// Schedule the first chunk read
$_.delay(read_next_chunk, 0);
} catch (e) {
state.error = e.toString();
state.downloading = false;
send(msg, {
type: 'error',
error: state.error,
url: msg.url
});
}
break;
case 'status':
console.log(`got status request. current is ${json.encode(get_status())}`)
send(msg, {
type: 'status_response',
...get_status()
});
break;
case 'cancel':
if (state.downloading) {
// Cancel the download
if (state.connection) {
http.fetch_close(state.connection);
state.connection = null;
}
state.downloading = false;
state.current_url = null;
state.download_msg = null;
state.chunks = [];
send(msg, {
type: 'cancelled',
message: 'Download cancelled',
url: state.current_url
});
} else {
send(msg, {
type: 'error',
error: 'No download in progress'
});
}
break;
default:
send(msg, {
type: 'error',
error: 'Unknown message type: ' + msg.type
});
}
});
// Non-blocking chunk reader
function read_next_chunk() {
if (!state.downloading || !state.connection) {
return;
}
try {
var chunk = http.fetch_read_chunk(state.connection);
if (chunk === null) {
// Download complete
finish_download();
return;
}
// Store chunk
state.chunks.push(chunk);
// Update progress
var info = http.fetch_info(state.connection);
state.downloaded_bytes = info.bytes_read;
if (info.headers_complete && info.content_length > 0) {
state.total_bytes = info.content_length;
}
// Schedule next chunk read
$_.delay(read_next_chunk, 0);
} catch (e) {
// Error during download
state.error = e.toString();
if (state.connection) {
http.fetch_close(state.connection);
}
if (state.download_msg) {
send(state.download_msg, {
type: 'error',
error: state.error,
url: state.current_url
});
}
// Reset state
state.downloading = false;
state.connection = null;
state.download_msg = null;
state.chunks = [];
}
}
// Complete the download and send result
function finish_download() {
if (state.connection) {
http.fetch_close(state.connection);
}
// Combine all chunks into single ArrayBuffer
var total_size = 0;
for (var i = 0; i < state.chunks.length; i++) {
total_size += state.chunks[i].byteLength;
}
var result = new ArrayBuffer(total_size);
var view = new Uint8Array(result);
var offset = 0;
for (var i = 0; i < state.chunks.length; i++) {
var chunk_view = new Uint8Array(state.chunks[i]);
view.set(chunk_view, offset);
offset += state.chunks[i].byteLength;
}
// Send complete message
if (state.download_msg) {
send(state.download_msg, {
type: 'complete',
url: state.current_url,
data: result,
size: result.byteLength,
duration: os.now() - state.start_time
});
}
// Reset state
state.downloading = false;
state.connection = null;
state.current_url = null;
state.download_msg = null;
state.chunks = [];
}