Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@appwrite.io/console",
"homepage": "https://appwrite.io/support",
"description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
"version": "13.0.0",
"version": "13.1.0",
"license": "BSD-3-Clause",
"main": "dist/cjs/sdk.js",
"exports": {
Expand Down
139 changes: 113 additions & 26 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,8 @@ class Client {
'x-sdk-name': 'Console',
'x-sdk-platform': 'console',
'x-sdk-language': 'web',
'x-sdk-version': '13.0.0',
'X-Appwrite-Response-Format': '1.9.4',
'x-sdk-version': '13.1.0',
'X-Appwrite-Response-Format': '1.9.5',
};

/**
Expand Down Expand Up @@ -978,44 +978,131 @@ class Client {
return await this.call(method, url, headers, originalPayload);
}

let start = 0;
let response = null;
const totalChunks = Math.ceil(file.size / Client.CHUNK_SIZE);

// Upload first chunk alone to get the upload ID
const firstChunkEnd = Math.min(Client.CHUNK_SIZE, file.size);
const firstChunkHeaders = { ...headers, 'content-range': `bytes 0-${firstChunkEnd - 1}/${file.size}` };
const firstChunk = file.slice(0, firstChunkEnd);
const firstPayload = { ...originalPayload };
firstPayload[fileParam] = new File([firstChunk], file.name);

let response = await this.call(method, url, firstChunkHeaders, firstPayload);
const uploadId = response?.$id;

if (onProgress && typeof onProgress === 'function') {
onProgress({
$id: uploadId,
progress: Math.round((firstChunkEnd / file.size) * 100),
sizeUploaded: firstChunkEnd,
chunksTotal: totalChunks,
chunksUploaded: 1
});
}

while (start < file.size) {
let end = start + Client.CHUNK_SIZE; // Prepare end for the next chunk
if (end >= file.size) {
end = file.size; // Adjust for the last chunk to include the last byte
}
if (totalChunks === 1) {
return response;
}

// Prepare remaining chunks
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const chunks: { start: number; end: number }[] = [];
for (let i = 1; i < totalChunks; i++) {
const start = i * Client.CHUNK_SIZE;
const end = Math.min(start + Client.CHUNK_SIZE, file.size);
chunks.push({ start, end });
}

headers['content-range'] = `bytes ${start}-${end-1}/${file.size}`;
const chunk = file.slice(start, end);
// Upload remaining chunks with max concurrency of 8
const CONCURRENCY = 8;
let completedCount = 1;
let uploadedBytes = firstChunkEnd;
let lastResponse = response;
let finalResponse = null;
let rejected = false;

const isUploadComplete = (chunkResponse: any) => {
const chunksUploaded = chunkResponse?.chunksUploaded;
const chunksTotal = chunkResponse?.chunksTotal ?? totalChunks;
return typeof chunksUploaded === 'number' && typeof chunksTotal === 'number' && chunksUploaded >= chunksTotal;
};

const uploadChunk = async (chunk: typeof chunks[0]) => {
const chunkHeaders = { ...headers };
if (uploadId) {
chunkHeaders['x-appwrite-id'] = uploadId;
}
chunkHeaders['content-range'] = `bytes ${chunk.start}-${chunk.end - 1}/${file.size}`;

const chunkBlob = file.slice(chunk.start, chunk.end);
const chunkPayload = { ...originalPayload };
chunkPayload[fileParam] = new File([chunkBlob], file.name);

let payload = { ...originalPayload };
payload[fileParam] = new File([chunk], file.name);
const chunkResponse = await this.call(method, url, chunkHeaders, chunkPayload);

response = await this.call(method, url, headers, payload);
if (rejected) {
return chunkResponse;
}

completedCount++;
uploadedBytes += (chunk.end - chunk.start);

lastResponse = chunkResponse;
if (isUploadComplete(chunkResponse)) {
finalResponse = chunkResponse;
}

if (onProgress && typeof onProgress === 'function') {
onProgress({
$id: response.$id,
progress: Math.round((end / file.size) * 100),
sizeUploaded: end,
chunksTotal: Math.ceil(file.size / Client.CHUNK_SIZE),
chunksUploaded: Math.ceil(end / Client.CHUNK_SIZE)
$id: uploadId,
progress: Math.round((uploadedBytes / file.size) * 100),
sizeUploaded: uploadedBytes,
chunksTotal: totalChunks,
chunksUploaded: completedCount
});
}

if (response && response.$id) {
headers['x-appwrite-id'] = response.$id;
}
return chunkResponse;
};

start = end;
}
await new Promise<void>((resolve, reject) => {
let nextChunk = 0;
let inFlight = 0;
let completed = 0;

const uploadNext = () => {
if (rejected) {
return;
}

if (completed === chunks.length) {
resolve();
return;
}

while (inFlight < CONCURRENCY && nextChunk < chunks.length) {
const chunk = chunks[nextChunk++];
inFlight++;

uploadChunk(chunk)
.then(() => {
inFlight--;
completed++;
uploadNext();
})
.catch((error) => {
rejected = true;
reject(error);
});
}
};

uploadNext();
});
Comment thread
greptile-apps[bot] marked this conversation as resolved.

return response;
return finalResponse ?? lastResponse;
}

async ping(): Promise<string> {
async ping(): Promise<unknown> {
return this.call('GET', new URL(this.config.endpoint + '/ping'));
}

Expand Down