App móvil Flutter para capturar contenido multimedia, etiquetarlo con hashes y enviarlo a backends configurables. Features: - Captura de fotos, audio, video y archivos - Sistema de etiquetas con bibliotecas externas (HST) - Packs de etiquetas predefinidos - Cola de reintentos (hasta 20 contenedores) - Soporte GPS - Hash SHA-256 auto-generado por contenedor - Persistencia SQLite local - Múltiples destinos configurables Stack: Flutter 3.38.5, flutter_bloc, sqflite, dio 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
120 lines
3.2 KiB
Dart
120 lines
3.2 KiB
Dart
import 'dart:convert';
|
|
import 'package:dio/dio.dart';
|
|
import '../../core/constants/app_constants.dart';
|
|
import '../../core/errors/exceptions.dart';
|
|
import '../../domain/entities/contenedor.dart';
|
|
import '../../domain/entities/destino.dart';
|
|
|
|
class BackendApi {
|
|
final Dio _dio;
|
|
|
|
BackendApi() : _dio = Dio() {
|
|
_dio.options.connectTimeout = AppConstants.httpTimeout;
|
|
_dio.options.receiveTimeout = AppConstants.httpTimeout;
|
|
}
|
|
|
|
Future<Map<String, dynamic>> enviarContenedor(
|
|
Contenedor contenedor,
|
|
Destino destino,
|
|
) async {
|
|
try {
|
|
final archivosJson = contenedor.archivos.map((a) => {
|
|
'nombre': a.nombre,
|
|
'tipo': a.mimeType,
|
|
'contenido': base64Encode(a.bytes),
|
|
}).toList();
|
|
|
|
final response = await _dio.post(
|
|
'${destino.url}/ingest',
|
|
options: Options(
|
|
headers: {
|
|
'X-Auth-Key': destino.hash,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
),
|
|
data: {
|
|
'hash': contenedor.hash,
|
|
if (contenedor.titulo != null) 'titulo': contenedor.titulo,
|
|
if (contenedor.descripcion != null) 'descripcion': contenedor.descripcion,
|
|
'etiquetas': contenedor.etiquetas,
|
|
if (contenedor.gps != null) 'gps': contenedor.gps!.toJson(),
|
|
'archivos': archivosJson,
|
|
},
|
|
);
|
|
|
|
return response.data as Map<String, dynamic>;
|
|
} on DioException catch (e) {
|
|
if (e.response?.statusCode == 409) {
|
|
throw HashExistsException();
|
|
}
|
|
throw NetworkException(
|
|
e.message ?? 'Network error',
|
|
code: e.response?.statusCode?.toString(),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<String> initChunkUpload(
|
|
String hash,
|
|
int totalChunks,
|
|
String fileName,
|
|
Destino destino,
|
|
) async {
|
|
try {
|
|
final response = await _dio.post(
|
|
'${destino.url}/upload/init',
|
|
options: Options(
|
|
headers: {'X-Auth-Key': destino.hash},
|
|
),
|
|
data: {
|
|
'hash': hash,
|
|
'total_chunks': totalChunks,
|
|
'file_name': fileName,
|
|
},
|
|
);
|
|
return response.data['upload_id'] as String;
|
|
} on DioException catch (e) {
|
|
throw NetworkException(e.message ?? 'Failed to init upload');
|
|
}
|
|
}
|
|
|
|
Future<void> uploadChunk(
|
|
String uploadId,
|
|
int chunkNumber,
|
|
List<int> bytes,
|
|
Destino destino,
|
|
) async {
|
|
try {
|
|
await _dio.post(
|
|
'${destino.url}/upload/chunk/$uploadId/$chunkNumber',
|
|
options: Options(
|
|
headers: {
|
|
'X-Auth-Key': destino.hash,
|
|
'Content-Type': 'application/octet-stream',
|
|
},
|
|
),
|
|
data: bytes,
|
|
);
|
|
} on DioException catch (e) {
|
|
throw NetworkException(e.message ?? 'Failed to upload chunk');
|
|
}
|
|
}
|
|
|
|
Future<Map<String, dynamic>> completeUpload(
|
|
String uploadId,
|
|
Destino destino,
|
|
) async {
|
|
try {
|
|
final response = await _dio.post(
|
|
'${destino.url}/upload/complete/$uploadId',
|
|
options: Options(
|
|
headers: {'X-Auth-Key': destino.hash},
|
|
),
|
|
);
|
|
return response.data as Map<String, dynamic>;
|
|
} on DioException catch (e) {
|
|
throw NetworkException(e.message ?? 'Failed to complete upload');
|
|
}
|
|
}
|
|
}
|