upload method

Future<RemoteUploadResult> upload(
  1. String url,
  2. RawContent content, {
  3. bool requiresAuth = true,
  4. String? ifMatch,
  5. required IriTerm documentIri,
})

Uploads content to url using HTTP PUT with DPoP authentication.

Uses content.contentType as the Content-Type header, fixing the previous hardcoded text/turtle limitation.

Implementation

Future<RemoteUploadResult> upload(
  String url,
  RawContent content, {
  bool requiresAuth = true,
  String? ifMatch,
  required IriTerm documentIri,
}) async {
  final dpop = requiresAuth
      ? await _authProvider.getDpopToken(_prepareUrlForDpopToken(url), 'PUT')
      : null;

  _log.fine(
      'PUT $url auth=$requiresAuth ifMatch=$ifMatch contentType=${content.contentType}');

  final response = await _client.put(
    Uri.parse(url),
    body: switch (content) {
      TextContent(:final text) => text,
      BinaryContent(:final bytes) => bytes,
    },
    headers: {
      'Content-Type': content.contentType,
      if (dpop != null) 'Authorization': 'DPoP ${dpop.accessToken}',
      if (dpop != null) 'DPoP': dpop.dPoP,
      if (ifMatch != null) 'If-Match': ifMatch,
      if (ifMatch == null) 'If-None-Match': '*',
    },
  );

  _log.fine('Response status: ${response.statusCode}');

  if (response.statusCode == 401) {
    _log.warning('401 Unauthorized for $url');
    throw AuthException(
      'Solid Pod authentication failed for $url',
      cause: 'HTTP 401 Unauthorized',
    );
  }
  if (response.statusCode == 404) {
    throw NotFoundException('Resource not found at $url');
  }
  if (response.statusCode == 409) {
    // 409 Conflict is not an ETag mismatch — it signals a persistent
    // server-side conflict (e.g. WebDAV lock, invalid resource state).
    // Retrying will not help, so treat as an error.
    throw SolidClientException('Failed to upload to $url: 409 Conflict');
  }
  if (response.statusCode == 412) {
    // 412 Precondition Failed: If-Match ETag mismatch — optimistic locking
    // conflict. The resource was modified concurrently; caller should
    // re-read, re-merge and retry.
    return RemoteUploadResult.conflict(
      documentIri: documentIri,
      requestETag: ifMatch,
    );
  }

  if (response.statusCode >= 200 && response.statusCode < 300) {
    var etag = response.headers['etag'];
    if (etag == null) {
      _log.fine('No ETag in PUT response from $url, fetching via HEAD');
      etag = await _fetchETag(url, requiresAuth: requiresAuth);
      if (etag == null) {
        _log.warning('Could not fetch ETag via HEAD for $url');
      }
    }
    return RemoteUploadResult.success(
      etag ?? '',
      documentIri: documentIri,
      requestETag: ifMatch,
    );
  }
  _log.warning('Failed to upload to $url: ${response.statusCode}');
  _log.warning('Response body: ${response.body}');
  throw SolidClientException(
      'Failed to upload to $url: ${response.statusCode}');
}