serveFile method

Future<bool> serveFile(
  1. File file,
  2. FileStat stat,
  3. RequestContext req,
  4. ResponseContext res,
)

Writes the contents of a file to a response.

Implementation

Future<bool> serveFile(
    File file, FileStat stat, RequestContext req, ResponseContext res) async {
  res.headers['accept-ranges'] = 'bytes';

  if (callback != null) {
    return await req.app?.executeHandler(
            (RequestContext req, ResponseContext res) =>
                callback!(file, req, res),
            req,
            res) ??
        true;
  }

  var type =
      app.mimeTypeResolver.lookup(file.path) ?? 'application/octet-stream';
  res.headers['accept-ranges'] = 'bytes';
  _ensureContentTypeAllowed(type, req);
  res.headers['accept-ranges'] = 'bytes';
  res.contentType = MediaType.parse(type);
  if (useBuffer == true) res.useBuffer();

  if (req.headers == null) {
    _log.severe('Missing headers in the RequestContext');
    throw ArgumentError('Missing headers in the RequestContext');
  }
  var reqHeaders = req.headers!;

  if (reqHeaders.value('range')?.startsWith('bytes=') != true) {
    await res.streamFile(file);
  } else {
    var header = RangeHeader.parse(reqHeaders.value('range')!);
    var items = RangeHeader.foldItems(header.items);
    header = RangeHeader(items);

    var totalFileSize = await file.length();

    for (var item in header.items) {
      var invalid = false;

      if (item.start != -1) {
        invalid = item.end != -1 && item.end < item.start;
      } else {
        invalid = item.end == -1;
      }

      if (invalid) {
        throw AngelHttpException(
            //Exception('Semantically invalid, or unbounded range.'),
            errors: ['Semantically invalid, or unbounded range.'],
            statusCode: 416,
            message: 'Semantically invalid, or unbounded range.');
      }

      // Ensure it's within range.
      if (item.start >= totalFileSize || item.end >= totalFileSize) {
        throw AngelHttpException(
            //Exception('Given range $item is out of bounds.'),
            errors: ['Given range $item is out of bounds.'],
            statusCode: 416,
            message: 'Given range $item is out of bounds.');
      }
    }

    if (header.items.isEmpty) {
      throw AngelHttpException(
          statusCode: 416, message: '`Range` header may not be empty.');
    } else if (header.items.length == 1) {
      var item = header.items[0];
      Stream<List<int>> stream;
      var len = 0;

      var total = totalFileSize;

      if (item.start == -1) {
        if (item.end == -1) {
          len = total;
          stream = file.openRead();
        } else {
          len = item.end + 1;
          stream = file.openRead(0, item.end + 1);
        }
      } else {
        if (item.end == -1) {
          len = total - item.start;
          stream = file.openRead(item.start);
        } else {
          len = item.end - item.start + 1;
          stream = file.openRead(item.start, item.end + 1);
        }
      }

      res.contentType = MediaType.parse(
          app.mimeTypeResolver.lookup(file.path) ??
              'application/octet-stream');
      res.statusCode = 206;
      res.headers['content-length'] = len.toString();
      res.headers['content-range'] = 'bytes ${item.toContentRange(total)}';
      await stream.cast<List<int>>().pipe(res);
      return false;
    } else {
      var transformer = RangeHeaderTransformer(
          header,
          app.mimeTypeResolver.lookup(file.path) ??
              'application/octet-stream',
          await file.length());
      res.statusCode = 206;
      res.headers['content-length'] =
          transformer.computeContentLength(totalFileSize).toString();
      res.contentType = MediaType(
          'multipart', 'byteranges', {'boundary': transformer.boundary});
      await file
          .openRead()
          .cast<List<int>>()
          .transform(transformer)
          .pipe(res);
      return false;
    }
  }

  return false;
}