Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PE-7176: Release ArDrive v2.58.0 #1925

Merged
merged 13 commits into from
Nov 21, 2024
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
1 change: 1 addition & 0 deletions android/fastlane/metadata/android/en-US/changelogs/164.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added support for uploading custom Arweave manifest files
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 @@ -507,6 +522,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 @@ -1094,6 +1112,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;
}
}
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: Secure, permanent storage

publish_to: 'none'

version: 2.57.3
version: 2.58.0

environment:
sdk: '>=3.2.0 <4.0.0'
Expand Down
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);
});
});
}
Loading