|
|
|
@ -168,58 +168,6 @@ public class ArbitraryResource {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@GET |
|
|
|
|
@Path("/raw/{signature}") |
|
|
|
|
@Operation( |
|
|
|
|
summary = "Fetch raw data associated with passed transaction signature", |
|
|
|
|
responses = { |
|
|
|
|
@ApiResponse( |
|
|
|
|
description = "raw data", |
|
|
|
|
content = @Content( |
|
|
|
|
schema = @Schema(type = "string", format = "byte"), |
|
|
|
|
mediaType = MediaType.APPLICATION_OCTET_STREAM |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
@ApiErrors({ |
|
|
|
|
ApiError.INVALID_SIGNATURE, ApiError.REPOSITORY_ISSUE, ApiError.TRANSACTION_INVALID |
|
|
|
|
}) |
|
|
|
|
public byte[] fetchRawData(@PathParam("signature") String signature58) { |
|
|
|
|
// Decode signature
|
|
|
|
|
byte[] signature; |
|
|
|
|
try { |
|
|
|
|
signature = Base58.decode(signature58); |
|
|
|
|
} catch (NumberFormatException e) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE, e); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try (final Repository repository = RepositoryManager.getRepository()) { |
|
|
|
|
TransactionData transactionData = repository.getTransactionRepository().fromSignature(signature); |
|
|
|
|
|
|
|
|
|
if (transactionData == null || transactionData.getType() != TransactionType.ARBITRARY) |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_SIGNATURE); |
|
|
|
|
|
|
|
|
|
ArbitraryTransactionData arbitraryTxData = (ArbitraryTransactionData) transactionData; |
|
|
|
|
|
|
|
|
|
// We're really expecting to only fetch the data's hash from repository
|
|
|
|
|
if (arbitraryTxData.getDataType() != DataType.DATA_HASH) |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID); |
|
|
|
|
|
|
|
|
|
ArbitraryTransaction arbitraryTx = new ArbitraryTransaction(repository, arbitraryTxData); |
|
|
|
|
|
|
|
|
|
// For now, we only allow locally stored data
|
|
|
|
|
if (!arbitraryTx.isDataLocal()) |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID); |
|
|
|
|
|
|
|
|
|
return arbitraryTx.fetchData(); |
|
|
|
|
} catch (ApiException e) { |
|
|
|
|
throw e; |
|
|
|
|
} catch (DataException e) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@POST |
|
|
|
|
@Operation( |
|
|
|
|
summary = "Build raw, unsigned, ARBITRARY transaction", |
|
|
|
@ -619,247 +567,4 @@ public class ArbitraryResource {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@DELETE |
|
|
|
|
@Path("/file") |
|
|
|
|
@Operation( |
|
|
|
|
summary = "Delete file using supplied base58 encoded SHA256 digest string", |
|
|
|
|
requestBody = @RequestBody( |
|
|
|
|
required = true, |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string", example = "FZdHKgF5CbN2tKihvop5Ts9vmWmA9ZyyPY6bC1zivjy4" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
), |
|
|
|
|
responses = { |
|
|
|
|
@ApiResponse( |
|
|
|
|
description = "true if deleted, false if not", |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
public String deleteFile(String hash58) { |
|
|
|
|
Security.checkApiCallAllowed(request); |
|
|
|
|
|
|
|
|
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash58(hash58); |
|
|
|
|
if (arbitraryDataFile.delete()) { |
|
|
|
|
return "true"; |
|
|
|
|
} |
|
|
|
|
return "false"; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@GET |
|
|
|
|
@Path("/file/{hash}/frompeer/{peer}") |
|
|
|
|
@Operation( |
|
|
|
|
summary = "Request file from a given peer, using supplied base58 encoded SHA256 hash", |
|
|
|
|
responses = { |
|
|
|
|
@ApiResponse( |
|
|
|
|
description = "true if retrieved, false if not", |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
@ApiErrors({ApiError.REPOSITORY_ISSUE, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.FILE_NOT_FOUND, ApiError.NO_REPLY}) |
|
|
|
|
public Response getFileFromPeer(@PathParam("hash") String hash58, |
|
|
|
|
@PathParam("peer") String targetPeerAddress) { |
|
|
|
|
try { |
|
|
|
|
if (hash58 == null) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
|
|
|
|
} |
|
|
|
|
if (targetPeerAddress == null) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Try to resolve passed address to make things easier
|
|
|
|
|
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress); |
|
|
|
|
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress(); |
|
|
|
|
List<Peer> peers = Network.getInstance().getHandshakedPeers(); |
|
|
|
|
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().toString().contains(resolvedAddress.toString())).findFirst().orElse(null); |
|
|
|
|
|
|
|
|
|
if (targetPeer == null) { |
|
|
|
|
LOGGER.info("Peer {} isn't connected", targetPeerAddress); |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
boolean success = this.requestFile(hash58, targetPeer); |
|
|
|
|
if (success) { |
|
|
|
|
return Response.ok("true").build(); |
|
|
|
|
} |
|
|
|
|
return Response.ok("false").build(); |
|
|
|
|
|
|
|
|
|
} catch (UnknownHostException e) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@POST |
|
|
|
|
@Path("/files/frompeer/{peer}") |
|
|
|
|
@Operation( |
|
|
|
|
summary = "Request multiple files from a given peer, using supplied comma separated base58 encoded SHA256 hashes", |
|
|
|
|
requestBody = @RequestBody( |
|
|
|
|
required = true, |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string", example = "FZdHKgF5CbN2tKihvop5Ts9vmWmA9ZyyPY6bC1zivjy4,FZdHKgF5CbN2tKihvop5Ts9vmWmA9ZyyPY6bC1zivjy4" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
), |
|
|
|
|
responses = { |
|
|
|
|
@ApiResponse( |
|
|
|
|
description = "true if retrieved, false if not", |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
@ApiErrors({ApiError.REPOSITORY_ISSUE, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.FILE_NOT_FOUND, ApiError.NO_REPLY}) |
|
|
|
|
public Response getFilesFromPeer(String files, @PathParam("peer") String targetPeerAddress) { |
|
|
|
|
try { |
|
|
|
|
if (targetPeerAddress == null) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Try to resolve passed address to make things easier
|
|
|
|
|
PeerAddress peerAddress = PeerAddress.fromString(targetPeerAddress); |
|
|
|
|
InetSocketAddress resolvedAddress = peerAddress.toSocketAddress(); |
|
|
|
|
List<Peer> peers = Network.getInstance().getHandshakedPeers(); |
|
|
|
|
Peer targetPeer = peers.stream().filter(peer -> peer.getResolvedAddress().toString().contains(resolvedAddress.toString())).findFirst().orElse(null); |
|
|
|
|
|
|
|
|
|
for (Peer peer : peers) { |
|
|
|
|
LOGGER.info("peer: {}", peer); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (targetPeer == null) { |
|
|
|
|
LOGGER.info("Peer {} isn't connected", targetPeerAddress); |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
String hash58List[] = files.split(","); |
|
|
|
|
for (String hash58 : hash58List) { |
|
|
|
|
if (hash58 != null) { |
|
|
|
|
boolean success = this.requestFile(hash58, targetPeer); |
|
|
|
|
if (!success) { |
|
|
|
|
LOGGER.info("Failed to request file {} from peer {}", hash58, targetPeerAddress); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return Response.ok("true").build(); |
|
|
|
|
|
|
|
|
|
} catch (UnknownHostException e) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private boolean requestFile(String hash58, Peer targetPeer) { |
|
|
|
|
try (final Repository repository = RepositoryManager.getRepository()) { |
|
|
|
|
|
|
|
|
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash58(hash58); |
|
|
|
|
if (arbitraryDataFile.exists()) { |
|
|
|
|
LOGGER.info("Data file {} already exists but we'll request it anyway", arbitraryDataFile); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
byte[] digest = null; |
|
|
|
|
try { |
|
|
|
|
digest = Base58.decode(hash58); |
|
|
|
|
} catch (NumberFormatException e) { |
|
|
|
|
LOGGER.info("Invalid base58 encoded string"); |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); |
|
|
|
|
} |
|
|
|
|
Message getArbitraryDataFileMessage = new GetArbitraryDataFileMessage(digest); |
|
|
|
|
|
|
|
|
|
Message message = targetPeer.getResponse(getArbitraryDataFileMessage); |
|
|
|
|
if (message == null) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
else if (message.getType() == Message.MessageType.BLOCK_SUMMARIES) { // TODO: use dedicated message type here
|
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ArbitraryDataFileMessage arbitraryDataFileMessage = (ArbitraryDataFileMessage) message; |
|
|
|
|
arbitraryDataFile = arbitraryDataFileMessage.getArbitraryDataFile(); |
|
|
|
|
if (arbitraryDataFile == null || !arbitraryDataFile.exists()) { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
LOGGER.info(String.format("Received file %s, size %d bytes", arbitraryDataFileMessage.getArbitraryDataFile(), arbitraryDataFileMessage.getArbitraryDataFile().size())); |
|
|
|
|
return true; |
|
|
|
|
} catch (ApiException e) { |
|
|
|
|
throw e; |
|
|
|
|
} catch (DataException | InterruptedException e) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@POST |
|
|
|
|
@Path("/file/{hash}/build") |
|
|
|
|
@Operation( |
|
|
|
|
summary = "Join multiple chunks into a single file, using supplied comma separated base58 encoded SHA256 digest strings", |
|
|
|
|
requestBody = @RequestBody( |
|
|
|
|
required = true, |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string", example = "FZdHKgF5CbN2tKihvop5Ts9vmWmA9ZyyPY6bC1zivjy4,FZdHKgF5CbN2tKihvop5Ts9vmWmA9ZyyPY6bC1zivjy4" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
), |
|
|
|
|
responses = { |
|
|
|
|
@ApiResponse( |
|
|
|
|
description = "true if joined, false if not", |
|
|
|
|
content = @Content( |
|
|
|
|
mediaType = MediaType.TEXT_PLAIN, |
|
|
|
|
schema = @Schema( |
|
|
|
|
type = "string" |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
) |
|
|
|
|
} |
|
|
|
|
) |
|
|
|
|
@ApiErrors({ApiError.REPOSITORY_ISSUE, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.FILE_NOT_FOUND, ApiError.NO_REPLY}) |
|
|
|
|
public Response joinFiles(String files, @PathParam("hash") String combinedHash) { |
|
|
|
|
|
|
|
|
|
if (combinedHash == null) { |
|
|
|
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash58(combinedHash); |
|
|
|
|
if (arbitraryDataFile.exists()) { |
|
|
|
|
LOGGER.info("We already have the combined file {}, but we'll join the chunks anyway.", combinedHash); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
String hash58List[] = files.split(","); |
|
|
|
|
for (String hash58 : hash58List) { |
|
|
|
|
if (hash58 != null) { |
|
|
|
|
ArbitraryDataFileChunk chunk = ArbitraryDataFileChunk.fromHash58(hash58); |
|
|
|
|
arbitraryDataFile.addChunk(chunk); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
boolean success = arbitraryDataFile.join(); |
|
|
|
|
if (success) { |
|
|
|
|
if (combinedHash.equals(arbitraryDataFile.digest58())) { |
|
|
|
|
LOGGER.info("Valid hash {} after joining {} files", arbitraryDataFile.digest58(), arbitraryDataFile.chunkCount()); |
|
|
|
|
return Response.ok("true").build(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return Response.ok("false").build(); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|