Skip to content

Commit 0c4322f

Browse files
committed
added content range header support on webdav GET petitions
1 parent 1cfb390 commit 0c4322f

6 files changed

Lines changed: 72 additions & 7 deletions

File tree

src/commands/download-file.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ export default class DownloadFile extends Command {
8080
user.mnemonic,
8181
driveFile.fileId,
8282
StreamUtils.writeStreamToWritableStream(fileWriteStream),
83+
undefined,
8384
{
8485
abortController: new AbortController(),
8586
progressCallback: (progress) => {

src/services/network/download.service.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ export class DownloadService {
55

66
async downloadFile(
77
url: string,
8-
options: { progressCallback?: (progress: number) => void; abortController?: AbortController },
8+
options: {
9+
progressCallback?: (progress: number) => void;
10+
abortController?: AbortController;
11+
rangeHeader?: string;
12+
},
913
): Promise<ReadableStream<Uint8Array>> {
1014
const response = await axios.get(url, {
1115
responseType: 'stream',
@@ -16,6 +20,9 @@ export class DownloadService {
1620
options.progressCallback(reportedProgress);
1721
}
1822
},
23+
headers: {
24+
range: options.rangeHeader,
25+
},
1926
});
2027

2128
const readable = new ReadableStream<Uint8Array>({

src/services/network/network-facade.service.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { DownloadService } from './download.service';
1717
import { ValidationService } from '../validation.service';
1818
import { HashStream } from '../../utils/hash.utils';
1919
import { ProgressTransform } from '../../utils/stream.utils';
20+
import { RangeOptions } from '../../utils/network.utils';
2021

2122
export class NetworkFacade {
2223
private readonly cryptoLib: Network.Crypto;
@@ -54,6 +55,7 @@ export class NetworkFacade {
5455
mnemonic: string,
5556
fileId: string,
5657
to: WritableStream,
58+
rangeOptions?: RangeOptions,
5759
options?: DownloadOptions,
5860
): Promise<[Promise<void>, AbortController]> {
5961
const encryptedContentStreams: ReadableStream<Uint8Array>[] = [];
@@ -80,6 +82,10 @@ export class NetworkFacade {
8082
};
8183

8284
const downloadFile: DownloadFileFunction = async (downloadables) => {
85+
if (rangeOptions && downloadables.length > 1) {
86+
throw new Error('Multi-Part Download with Range-Requests is not implemented');
87+
}
88+
8389
for (const downloadable of downloadables) {
8490
if (abortable.signal.aborted) {
8591
throw new Error('Download aborted');
@@ -88,6 +94,7 @@ export class NetworkFacade {
8894
const encryptedContentStream = await this.downloadService.downloadFile(downloadable.url, {
8995
progressCallback: onDownloadProgress,
9096
abortController: options?.abortController,
97+
rangeHeader: rangeOptions?.range,
9198
});
9299

93100
encryptedContentStreams.push(encryptedContentStream);

src/utils/network.utils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { createHash, X509Certificate } from 'node:crypto';
33
import { readFile, stat, writeFile } from 'node:fs/promises';
44
import path from 'node:path';
55
import selfsigned from 'selfsigned';
6+
import parseRange from 'range-parser';
67
import { ConfigService } from '../services/config.service';
78

89
export class NetworkUtils {
@@ -74,4 +75,41 @@ export class NetworkUtils {
7475
const pems = selfsigned.generate(attrs, { days: 365, algorithm: 'sha256', keySize: 2048, extensions });
7576
return pems;
7677
}
78+
79+
static parseRangeHeader(rangeOptions: { range?: string; totalFileSize: number }): RangeOptions | undefined {
80+
if (!rangeOptions.range) {
81+
return;
82+
}
83+
const parsed = parseRange(rangeOptions.totalFileSize, rangeOptions.range);
84+
if (Array.isArray(parsed)) {
85+
if (parsed.length > 1) {
86+
throw new Error(`Multi Range-Requests functionality is not implemented. ${JSON.stringify(rangeOptions)}`);
87+
} else if (parsed.length <= 0) {
88+
throw new Error(`Empty Range-Request. ${JSON.stringify(rangeOptions)}`);
89+
} else if (parsed.type !== 'bytes') {
90+
throw new Error(`Unkwnown Range-Request type "${parsed.type}". ${JSON.stringify(rangeOptions)}`);
91+
} else {
92+
const rangeSize = parsed[0].end - parsed[0].start + 1;
93+
return {
94+
range: rangeOptions.range,
95+
rangeSize: rangeSize,
96+
totalFileSize: rangeOptions.totalFileSize,
97+
parsed: parsed[0],
98+
};
99+
}
100+
} else if (parsed === -1) {
101+
throw new Error(`Malformed Range-Request. ${JSON.stringify(rangeOptions)}`);
102+
} else if (parsed === -2) {
103+
throw new Error(`Unsatisfiable Range-Request. ${JSON.stringify(rangeOptions)}`);
104+
} else {
105+
throw new Error(`Unknown error from Range-Request. ${JSON.stringify(rangeOptions)}`);
106+
}
107+
}
108+
}
109+
110+
export interface RangeOptions {
111+
range: string;
112+
rangeSize: number;
113+
totalFileSize: number;
114+
parsed: parseRange.Range;
77115
}

src/webdav/handlers/GET.handler.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { UploadService } from '../../services/network/upload.service';
88
import { DownloadService } from '../../services/network/download.service';
99
import { CryptoService } from '../../services/crypto.service';
1010
import { AuthService } from '../../services/auth.service';
11-
import { NotFoundError, NotImplementedError } from '../../utils/errors.utils';
11+
import { NotFoundError } from '../../utils/errors.utils';
1212
import { webdavLogger } from '../../utils/logger.utils';
1313
import { DriveFileItem } from '../../types/drive.types';
14+
import { NetworkUtils } from '../../utils/network.utils';
1415

1516
export class GETRequestHandler implements WebDavMethodHandler {
1617
constructor(
@@ -29,8 +30,6 @@ export class GETRequestHandler implements WebDavMethodHandler {
2930
const { driveDatabaseManager, driveFileService, authService, networkFacade } = this.dependencies;
3031
const resource = await WebDavUtils.getRequestedResource(req);
3132

32-
if (req.headers['content-range'] || req.headers['range'])
33-
throw new NotImplementedError('Range requests not supported');
3433
if (resource.name.startsWith('._')) throw new NotFoundError('File not found');
3534

3635
webdavLogger.info(`GET request received for file at ${resource.url}`);
@@ -42,9 +41,6 @@ export class GETRequestHandler implements WebDavMethodHandler {
4241

4342
webdavLogger.info(`✅ Found Drive File with uuid ${driveFile.uuid}`);
4443

45-
res.set('Content-Type', 'application/octet-stream');
46-
res.set('Content-length', driveFile.size.toString());
47-
4844
const { user } = await authService.getAuthDetails();
4945
webdavLogger.info('✅ Network ready for download');
5046

@@ -57,12 +53,27 @@ export class GETRequestHandler implements WebDavMethodHandler {
5753
},
5854
});
5955

56+
const range = req.headers['range'];
57+
const rangeOptions = NetworkUtils.parseRangeHeader({
58+
range,
59+
totalFileSize: driveFile.size,
60+
});
61+
let contentLength = driveFile.size;
62+
if (rangeOptions) {
63+
webdavLogger.info('✅ Range request received:', { rangeOptions });
64+
contentLength = rangeOptions.rangeSize;
65+
}
66+
67+
res.set('Content-Type', 'application/octet-stream');
68+
res.set('Content-length', contentLength.toString());
69+
6070
let lastLoggedProgress = 0;
6171
const [executeDownload] = await networkFacade.downloadToStream(
6272
driveFile.bucket,
6373
user.mnemonic,
6474
driveFile.fileId,
6575
writable,
76+
rangeOptions,
6677
{
6778
progressCallback: (progress) => {
6879
const percentage = Math.floor(100 * progress);

test/services/network/network-facade.service.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ describe('Network Facade Service', () => {
224224
'index course habit soon assist dragon tragic helmet salute stuff later twice consider grit pulse cement obvious trick sponsor stereo hello win royal more',
225225
'f1858bc9675f9e4f7ab29429',
226226
writable,
227+
undefined,
227228
options,
228229
);
229230

0 commit comments

Comments
 (0)