Skip to content

Commit

Permalink
Merge pull request #1914 from ardriveapp/PE-7106-upload-custom-manifests
Browse files Browse the repository at this point in the history
PE-7106: feat(custom manifest)
  • Loading branch information
thiagocarvalhodev authored Nov 21, 2024
2 parents 6bb1367 + b489cfa commit acf0e5b
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 3 deletions.
33 changes: 33 additions & 0 deletions lib/blocs/upload/upload_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:ardrive/core/activity_tracker.dart';
import 'package:ardrive/core/upload/domain/repository/upload_repository.dart';
import 'package:ardrive/core/upload/uploader.dart';
import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart';
import 'package:ardrive/entities/constants.dart';
import 'package:ardrive/main.dart';
import 'package:ardrive/manifest/domain/manifest_repository.dart';
import 'package:ardrive/models/forms/cc.dart';
Expand All @@ -21,6 +22,7 @@ import 'package:ardrive/services/config/config_service.dart';
import 'package:ardrive/services/license/license.dart';
import 'package:ardrive/turbo/services/upload_service.dart';
import 'package:ardrive/utils/constants.dart';
import 'package:ardrive/utils/is_custom_manifest.dart';
import 'package:ardrive/utils/logger.dart';
import 'package:ardrive/utils/plausible_event_tracker/plausible_custom_event_properties.dart';
import 'package:ardrive/utils/plausible_event_tracker/plausible_event_tracker.dart';
Expand Down Expand Up @@ -111,6 +113,9 @@ class UploadCubit extends Cubit<UploadState> {

bool _isManifestsUploadCancelled = false;

/// if true, the file will change its content type to `application/x.arweave-manifest+json`
bool _uploadFileAsCustomManifest = false;

void updateManifestSelection(List<ManifestSelection> selections) {
_selectedManifestModels.clear();

Expand All @@ -126,6 +131,11 @@ class UploadCubit extends Cubit<UploadState> {
_manifestUploadMethod = method;
}

void setIsUploadingCustomManifest(bool value) {
_uploadFileAsCustomManifest = value;
emit((state as UploadReady).copyWith(uploadFileAsCustomManifest: value));
}

Future<void> prepareManifestUpload() async {
final manifestModels = _selectedManifestModels
.map((e) => UploadManifestModel(
Expand Down Expand Up @@ -441,6 +451,9 @@ class UploadCubit extends Cubit<UploadState> {
bool showArnsCheckbox = false;

if (_targetDrive.isPublic && _files.length == 1) {
final fileIsACustomManifest =
await isCustomManifest(_files.first.ioFile);

emit(
UploadReady(
params: (state as UploadReadyToPrepare).params,
Expand All @@ -461,6 +474,8 @@ class UploadCubit extends Cubit<UploadState> {
arnsRecords: _ants,
showReviewButtonText: false,
selectedManifestSelections: _selectedManifestModels,
shouldShowCustomManifestCheckbox: fileIsACustomManifest,
uploadFileAsCustomManifest: false,
),
);

Expand Down Expand Up @@ -505,6 +520,9 @@ class UploadCubit extends Cubit<UploadState> {
canShowSettings: showSettings,
showReviewButtonText: false,
selectedManifestSelections: _selectedManifestModels,
uploadFileAsCustomManifest: false,
// only applies for single file uploads
shouldShowCustomManifestCheckbox: false,
),
);
}
Expand Down Expand Up @@ -1092,6 +1110,21 @@ class UploadCubit extends Cubit<UploadState> {
return;
}

if (_uploadFileAsCustomManifest) {
final fileWithCustomContentType = await IOFile.fromData(
await _files.first.ioFile.readAsBytes(),
name: _files.first.ioFile.name,
lastModifiedDate: _files.first.ioFile.lastModifiedDate,
contentType: ContentType.manifest,
);

_files.first = UploadFile(
ioFile: fileWithCustomContentType,
parentFolderId: _files.first.parentFolderId,
relativeTo: _files.first.relativeTo,
);
}

_uploadIsInProgress = true;

UploadPlan uploadPlan;
Expand Down
10 changes: 10 additions & 0 deletions lib/blocs/upload/upload_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ class UploadReady extends UploadState {

final bool isArConnect;
final bool showReviewButtonText;
final bool shouldShowCustomManifestCheckbox;
final bool uploadFileAsCustomManifest;

UploadReady({
required this.paymentInfo,
Expand All @@ -136,6 +138,8 @@ class UploadReady extends UploadState {
required this.arnsRecords,
required this.showReviewButtonText,
required this.selectedManifestSelections,
required this.shouldShowCustomManifestCheckbox,
required this.uploadFileAsCustomManifest,
});

// copyWith
Expand All @@ -160,6 +164,8 @@ class UploadReady extends UploadState {
List<ANTRecord>? arnsRecords,
bool? showReviewButtonText,
List<ManifestSelection>? selectedManifestSelections,
bool? shouldShowCustomManifestCheckbox,
bool? uploadFileAsCustomManifest,
}) {
return UploadReady(
loadingArNSNames: loadingArNSNames ?? this.loadingArNSNames,
Expand All @@ -184,6 +190,10 @@ class UploadReady extends UploadState {
showReviewButtonText: showReviewButtonText ?? this.showReviewButtonText,
selectedManifestSelections:
selectedManifestSelections ?? this.selectedManifestSelections,
shouldShowCustomManifestCheckbox: shouldShowCustomManifestCheckbox ??
this.shouldShowCustomManifestCheckbox,
uploadFileAsCustomManifest:
uploadFileAsCustomManifest ?? this.uploadFileAsCustomManifest,
);
}

Expand Down
18 changes: 16 additions & 2 deletions lib/components/upload_form.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// ignore_for_file: use_build_context_synchronously

import 'dart:async';
import 'dart:math';

Expand Down Expand Up @@ -1977,6 +1975,22 @@ class _UploadReadyWidget extends StatelessWidget {
),
),
],
if (state.shouldShowCustomManifestCheckbox) ...[
const SizedBox(height: 8),
ArDriveCheckBox(
title: 'Convert this file to an Arweave manifest.',
checked: state.uploadFileAsCustomManifest,
useNewIcons: true,
titleStyle: typography.paragraphNormal(
fontWeight: ArFontWeight.semiBold,
),
onChange: (value) {
context
.read<UploadCubit>()
.setIsUploadingCustomManifest(value);
},
),
],
],
);
},
Expand Down
1 change: 1 addition & 0 deletions lib/services/arweave/arweave_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ class ArweaveService {

yield licenseComposedQuery.data!.transactions.edges
.map((e) => e.node)
.where((e) => e.tags.any((t) => t.name == 'License'))
.toList();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
query LicenseComposed($transactionIds: [ID!]) {
transactions(
ids: $transactionIds
tags: [{ name: "License", values: [""], op: NEQ }]
) {
edges {
node {
Expand Down
40 changes: 40 additions & 0 deletions lib/utils/is_custom_manifest.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'dart:convert';

import 'package:ardrive/utils/logger.dart';
import 'package:ardrive_io/ardrive_io.dart';

/// Checks if a file is an Arweave manifest file by examining its content type and contents.
///
/// Returns true if the file has JSON content type and contains the string "arweave/paths",
/// which indicates it follows the Arweave path manifest specification.
Future<bool> isCustomManifest(IOFile file) async {
try {
if (file.contentType == 'application/json') {
final fileLength = await file.length;

int bytesToRead = 100;

if (fileLength < bytesToRead) {
bytesToRead = fileLength;
}

/// Read the first 100 bytes of the file
final first100Bytes = file.openReadStream(0, bytesToRead);

String content = '';

await for (var bytes in first100Bytes) {
content += utf8.decode(bytes);
}

/// verify if file contains "arweave/paths"
if (content.contains('arweave/paths')) {
return true;
}
}
return false;
} catch (e) {
logger.e('Error checking if file is a custom manifest', e);
return false;
}
}
84 changes: 84 additions & 0 deletions test/utils/is_custom_manifest_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:ardrive/utils/is_custom_manifest.dart';
import 'package:ardrive_io/ardrive_io.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

class MockIOFile extends Mock implements IOFile {}

void main() {
late MockIOFile mockFile;

setUp(() {
mockFile = MockIOFile();
registerFallbackValue(0);
registerFallbackValue(100);
});

group('isCustomManifest', () {
test('returns true when file is JSON and contains arweave/paths', () async {
// Arrange
const jsonContent =
'{"manifest":"arweave/paths","version":"0.1.0","index":{"path":"hello_world.html"},"paths":{"hello_world.html":{"id":"KlwrMWFW9ckVKa8pCGk9a8EjwzYZ7jNVUVHdcE2YkHo"}}}';
final bytes = utf8.encode(jsonContent);

when(() => mockFile.contentType).thenReturn('application/json');
when(() => mockFile.openReadStream(any(), any()))
.thenAnswer((_) => Stream.value(Uint8List.fromList(bytes)));
when(() => mockFile.length).thenReturn(bytes.length);
// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, true);
verify(() => mockFile.openReadStream(0, 100)).called(1);
});

test('returns false when file is JSON but does not contain arweave/paths',
() async {
// Arrange
const jsonContent = '{"version": 1, "type": "regular"}';
final bytes = utf8.encode(jsonContent);

when(() => mockFile.contentType).thenReturn('application/json');
when(() => mockFile.openReadStream(any(), any()))
.thenAnswer((_) => Stream.value(Uint8List.fromList(bytes)));
when(() => mockFile.length).thenReturn(bytes.length);

// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, false);
verify(() => mockFile.openReadStream(0, 33)).called(1);
});

test('returns false when file is not JSON', () async {
// Arrange
when(() => mockFile.contentType).thenReturn('text/plain');
when(() => mockFile.length).thenReturn(0);
// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, false);
verifyNever(() => mockFile.openReadStream(any(), any()));
});

test('returns false when stream is empty', () async {
// Arrange
when(() => mockFile.contentType).thenReturn('application/json');
when(() => mockFile.openReadStream(any(), any()))
.thenAnswer((_) => Stream.value(Uint8List(0)));
when(() => mockFile.length).thenReturn(0);
// Act
final result = await isCustomManifest(mockFile);

// Assert
expect(result, false);
verify(() => mockFile.openReadStream(0, 0)).called(1);
});
});
}

0 comments on commit acf0e5b

Please sign in to comment.