mirror of
https://github.com/Qortal/qortal.git
synced 2025-08-02 00:31:25 +00:00
Compare commits
208 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
dd991cebbd | ||
|
2370a67b8a | ||
|
0993903aa0 | ||
|
f5e9b91d6b | ||
|
7fe507a497 | ||
|
10f12221c9 | ||
|
85980e4cfc | ||
|
7bb6b84e86 | ||
|
dc25d33739 | ||
|
358e67b050 | ||
|
8331241d75 | ||
|
e041748b48 | ||
|
06691af729 | ||
|
cfe6dfcd1c | ||
|
3f00cda847 | ||
|
a286db2dfd | ||
|
28bd4adcd2 | ||
|
61b7cdd025 | ||
|
250245d5e1 | ||
|
0258d2bcb6 | ||
|
735de93848 | ||
|
57485bfe36 | ||
|
ed05560413 | ||
|
892b667f86 | ||
|
ea7a2224d3 | ||
|
20893879ca | ||
|
b08e845dbb | ||
|
e60cd96514 | ||
|
e2a2a1f956 | ||
|
7f53983d77 | ||
|
ce52b39495 | ||
|
3296779125 | ||
|
3dcd9d237c | ||
|
23ec71d7be | ||
|
5bbde4dcdb | ||
|
dc2da8b283 | ||
|
f3772d19f5 | ||
|
35def54ecc | ||
|
2086a2c476 | ||
|
4835e5732d | ||
|
d831972005 | ||
|
f6914821d3 | ||
|
073d124aef | ||
|
a83e332c11 | ||
|
7deb9328fa | ||
|
e598d7476b | ||
|
85735fabb2 | ||
|
7392082875 | ||
|
88f8041b05 | ||
|
3109c3bb16 | ||
|
8d462dedfa | ||
|
fdd9741936 | ||
|
929d0ac897 | ||
|
952d18390b | ||
|
bc026d9d1c | ||
|
ea2577d1c3 | ||
|
c78593cf15 | ||
|
de4523c34e | ||
|
b08329dcf1 | ||
|
668be633c4 | ||
|
ea6225ab9a | ||
|
055b66e835 | ||
|
2a7a2d3220 | ||
|
73a7c1fe7e | ||
|
2848ae695c | ||
|
713fd4f0c6 | ||
|
519bb10c60 | ||
|
3a64336d9f | ||
|
5ecc633fd7 | ||
|
1b9afce21f | ||
|
f9f34a61ac | ||
|
46b225cdfb | ||
|
4ce3b2a786 | ||
|
87ed49a2ee | ||
|
a555f503eb | ||
|
50780aba53 | ||
|
2bee3cbb5c | ||
|
534a44d0ce | ||
|
469c1af0ef | ||
|
5656100197 | ||
|
d9cac6db39 | ||
|
98b0b1932d | ||
|
9968865d0e | ||
|
05eb337367 | ||
|
5386db8a3f | ||
|
edae7fd844 | ||
|
4840804d32 | ||
|
b5cb5f1da3 | ||
|
101023ba1d | ||
|
ed73162881 | ||
|
0388626e42 | ||
|
c5c0dcf0f2 | ||
|
384f592f59 | ||
|
1528e05e0b | ||
|
82c66c0555 | ||
|
b5ce8d5fb3 | ||
|
b4a736c5d2 | ||
|
4afbca7ed2 | ||
|
44aa0a6026 | ||
|
b1452bddf3 | ||
|
96ac883515 | ||
|
b6803490b9 | ||
|
3739920ad3 | ||
|
7f21ea7e00 | ||
|
83b0ce53e6 | ||
|
d6ab9eb066 | ||
|
ac60ef30a3 | ||
|
94f14a39e3 | ||
|
4b7844dc06 | ||
|
c40d0cc67b | ||
|
3318093a4f | ||
|
cf0681d7df | ||
|
7d7cea3278 | ||
|
7d38fa909d | ||
|
0b05de22a0 | ||
|
308196250e | ||
|
b254ca7706 | ||
|
9ea2d7ab09 | ||
|
d166f625d0 | ||
|
8e2dd60ea0 | ||
|
d51f9368ef | ||
|
b17035c864 | ||
|
fa14568cb9 | ||
|
64cd21b0dd | ||
|
abdc265fc6 | ||
|
1153519d78 | ||
|
0af6fbe1eb | ||
|
d54006caf7 | ||
|
e1771dbaea | ||
|
cc98abeffb | ||
|
a3702ac6b0 | ||
|
c1ffe557e1 | ||
|
c310a7c5e8 | ||
|
c5a0b00cde | ||
|
69902f7f5b | ||
|
999e8b8aca | ||
|
466c727dee | ||
|
ba9f3b335c | ||
|
148ca0af05 | ||
|
c39b9c764b | ||
|
d30eb6141a | ||
|
52c806f9e6 | ||
|
b2d31a7e02 | ||
|
cfa0b1d8ea | ||
|
edacce1bac | ||
|
074cba2266 | ||
|
7f23ef64a2 | ||
|
5b7e9666dc | ||
|
f4a32d19dd | ||
|
eb6d84c04d | ||
|
26587067d8 | ||
|
227d93a31e | ||
|
76f17dda53 | ||
|
830bae3dc1 | ||
|
ec09312cc5 | ||
|
11654ba9c6 | ||
|
ea356d1026 | ||
|
ae5b713e58 | ||
|
257ca2da05 | ||
|
d27316eb64 | ||
|
64d8353629 | ||
|
3077810ea8 | ||
|
4ba2f7ad6a | ||
|
8eba0f89fe | ||
|
600f98ddab | ||
|
eb07e6613f | ||
|
6c445ff646 | ||
|
4d9cece9fa | ||
|
8beffd4dae | ||
|
566c6a3f4b | ||
|
1be3ae267e | ||
|
7af551fbc5 | ||
|
6ba6c58843 | ||
|
ca09dd264f | ||
|
eea98d0bc7 | ||
|
9c58faa7c2 | ||
|
3cdfa4e276 | ||
|
380ba5b8c2 | ||
|
04f248bcdd | ||
|
37b20aac66 | ||
|
e1e52b3165 | ||
|
46e8baac98 | ||
|
3b6e1ea27f | ||
|
5a1cc7a0de | ||
|
0ec5e39517 | ||
|
bede5a71f8 | ||
|
5e750b4283 | ||
|
4a42dc2d00 | ||
|
d7b1615d4f | ||
|
8c41a4a6b3 | ||
|
8dffe1e3ac | ||
|
932a553b91 | ||
|
57eacbdd59 | ||
|
86d6037af3 | ||
|
ca80fd5f9c | ||
|
03a54691a1 | ||
|
3c8088e463 | ||
|
de47a94677 | ||
|
bd4c47dba6 | ||
|
2c78f4b45b | ||
|
613ce84df8 | ||
|
2822d860d8 | ||
|
5a052a4f67 | ||
|
32c2f68cb1 | ||
|
4232616a5f | ||
|
8ddcae249c | ||
|
1abceada20 | ||
|
4c463f65b7 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,7 +14,6 @@
|
||||
/.mvn.classpath
|
||||
/notes*
|
||||
/settings.json
|
||||
/testnet*
|
||||
/settings*.json
|
||||
/testchain*.json
|
||||
/run-testnet*.sh
|
||||
|
3
Q-Apps.md
Normal file
3
Q-Apps.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Qortal Project - Q-Apps Documentation
|
||||
|
||||
The Q-Apps documentation has moved [here](https://github.com/Qortal/qortal/blob/master/Q-Apps.md).
|
@@ -17,10 +17,10 @@
|
||||
<ROW Property="Manufacturer" Value="Qortal"/>
|
||||
<ROW Property="MsiLogging" MultiBuildValue="DefaultBuild:vp"/>
|
||||
<ROW Property="NTP_GOOD" Value="false"/>
|
||||
<ROW Property="ProductCode" Value="1033:{6C93A96C-E3AF-42FD-BE11-7EC3734905C6} 1049:{754F5347-82E5-4251-AED0-F4141CDD11F5} 2052:{413BD7B3-A3F8-47D0-BCA4-5C7694A40936} 2057:{71450AC8-1E6F-4469-852D-0591FA693680} " Type="16"/>
|
||||
<ROW Property="ProductCode" Value="1033:{CB85115E-ECCE-4B3D-BB7F-6251A2764922} 1049:{09AC1C62-4E33-4312-826A-38F597ED1B17} 2052:{3CF701B3-E118-4A31-A4B7-156CEA19FBCC} 2057:{468F337D-0EF8-41D1-B5DE-4EEE66BA2AF6} " Type="16"/>
|
||||
<ROW Property="ProductLanguage" Value="2057"/>
|
||||
<ROW Property="ProductName" Value="Qortal"/>
|
||||
<ROW Property="ProductVersion" Value="3.8.3" Type="32"/>
|
||||
<ROW Property="ProductVersion" Value="3.8.5" Type="32"/>
|
||||
<ROW Property="RECONFIG_NTP" Value="true"/>
|
||||
<ROW Property="REMOVE_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
<ROW Property="REPAIR_BLOCKCHAIN" Value="YES" Type="4"/>
|
||||
@@ -212,7 +212,7 @@
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_71" ComponentId="{12A3ADBE-BB7A-496C-8869-410681E6232F}" Directory_="jdk.zipfs_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_71" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_8" ComponentId="{D53AD95E-CF96-4999-80FC-5812277A7456}" Directory_="java.naming_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_8" Type="0"/>
|
||||
<ROW Component="ADDITIONAL_LICENSE_INFO_9" ComponentId="{6B7EA9B0-5D17-47A8-B78C-FACE86D15E01}" Directory_="java.net.http_Dir" Attributes="0" KeyPath="ADDITIONAL_LICENSE_INFO_9" Type="0"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{EC7B4AD9-F2D9-48C4-A586-C4697D9C380C}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_CustomARPName" ComponentId="{094B5D07-2258-4A39-9917-2E2F7F6E210B}" Directory_="APPDIR" Attributes="260" KeyPath="DisplayName" Options="1"/>
|
||||
<ROW Component="AI_ExePath" ComponentId="{3644948D-AE0B-41BB-9FAF-A79E70490A08}" Directory_="APPDIR" Attributes="260" KeyPath="AI_ExePath"/>
|
||||
<ROW Component="APPDIR" ComponentId="{680DFDDE-3FB4-47A5-8FF5-934F576C6F91}" Directory_="APPDIR" Attributes="0"/>
|
||||
<ROW Component="AccessBridgeCallbacks.h" ComponentId="{288055D1-1062-47A3-AA44-5601B4E38AED}" Directory_="bridge_Dir" Attributes="0" KeyPath="AccessBridgeCallbacks.h" Type="0"/>
|
||||
|
9
pom.xml
9
pom.xml
@@ -3,7 +3,7 @@
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>org.qortal</groupId>
|
||||
<artifactId>qortal</artifactId>
|
||||
<version>3.8.5</version>
|
||||
<version>3.9.1</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<skipTests>true</skipTests>
|
||||
@@ -36,6 +36,7 @@
|
||||
<java-diff-utils.version>4.10</java-diff-utils.version>
|
||||
<grpc.version>1.45.1</grpc.version>
|
||||
<protobuf.version>3.19.4</protobuf.version>
|
||||
<simplemagic.version>1.17</simplemagic.version>
|
||||
</properties>
|
||||
<build>
|
||||
<sourceDirectory>src/main/java</sourceDirectory>
|
||||
@@ -147,6 +148,7 @@
|
||||
tagsSorter: "alpha",
|
||||
operationsSorter:
|
||||
"alpha",
|
||||
validatorUrl: false,
|
||||
</value>
|
||||
</replacement>
|
||||
</replacements>
|
||||
@@ -728,5 +730,10 @@
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.j256.simplemagic</groupId>
|
||||
<artifactId>simplemagic</artifactId>
|
||||
<version>${simplemagic.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@@ -79,7 +79,7 @@ public enum ApiError {
|
||||
// BUYER_ALREADY_OWNER(411, 422),
|
||||
|
||||
// POLLS
|
||||
// POLL_NO_EXISTS(501, 404),
|
||||
POLL_NO_EXISTS(501, 404),
|
||||
// POLL_ALREADY_EXISTS(502, 422),
|
||||
// DUPLICATE_OPTION(503, 422),
|
||||
// POLL_OPTION_NO_EXISTS(504, 404),
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.api;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
@@ -20,14 +21,12 @@ import javax.net.ssl.SNIHostName;
|
||||
import javax.net.ssl.SNIServerName;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.xml.bind.JAXBContext;
|
||||
import javax.xml.bind.JAXBException;
|
||||
import javax.xml.bind.UnmarshalException;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.*;
|
||||
import javax.xml.transform.stream.StreamSource;
|
||||
|
||||
import org.eclipse.persistence.exceptions.XMLMarshalException;
|
||||
import org.eclipse.persistence.jaxb.JAXBContextFactory;
|
||||
import org.eclipse.persistence.jaxb.MarshallerProperties;
|
||||
import org.eclipse.persistence.jaxb.UnmarshallerProperties;
|
||||
|
||||
public class ApiRequest {
|
||||
@@ -107,6 +106,36 @@ public class ApiRequest {
|
||||
}
|
||||
}
|
||||
|
||||
private static Marshaller createMarshaller(Class<?> objectClass) {
|
||||
try {
|
||||
// Create JAXB context aware of object's class
|
||||
JAXBContext jc = JAXBContextFactory.createContext(new Class[] { objectClass }, null);
|
||||
|
||||
// Create marshaller
|
||||
Marshaller marshaller = jc.createMarshaller();
|
||||
|
||||
// Set the marshaller media type to JSON
|
||||
marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json");
|
||||
|
||||
// Tell marshaller not to include JSON root element in the output
|
||||
marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, false);
|
||||
|
||||
return marshaller;
|
||||
} catch (JAXBException e) {
|
||||
throw new RuntimeException("Unable to create API marshaller", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void marshall(Writer writer, Object object) throws IOException {
|
||||
Marshaller marshaller = createMarshaller(object.getClass());
|
||||
|
||||
try {
|
||||
marshaller.marshal(object, writer);
|
||||
} catch (JAXBException e) {
|
||||
throw new IOException("Unable to create marshall object for API", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getParamsString(Map<String, String> params) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
|
||||
|
@@ -13,8 +13,8 @@ import java.security.SecureRandom;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.checkerframework.checker.units.qual.A;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RedirectPatternRule;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
@@ -41,6 +41,7 @@ import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.api.websocket.*;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
public class ApiService {
|
||||
@@ -51,9 +52,11 @@ public class ApiService {
|
||||
private Server server;
|
||||
private ApiKey apiKey;
|
||||
|
||||
public static final String API_VERSION_HEADER = "X-API-VERSION";
|
||||
|
||||
private ApiService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.resource");
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.restricted.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
@@ -123,13 +126,13 @@ public class ApiService {
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getApiPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getApiPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
@@ -230,4 +233,19 @@ public class ApiService {
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
public static int getApiVersion(HttpServletRequest request) {
|
||||
// Get API version
|
||||
String apiVersionString = request.getHeader(API_VERSION_HEADER);
|
||||
if (apiVersionString == null) {
|
||||
// Try query string - this is needed to avoid a CORS preflight. See: https://stackoverflow.com/a/43881141
|
||||
apiVersionString = request.getParameter("apiVersion");
|
||||
}
|
||||
|
||||
int apiVersion = 1;
|
||||
if (apiVersionString != null) {
|
||||
apiVersion = Integer.parseInt(apiVersionString);
|
||||
}
|
||||
return apiVersion;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ package org.qortal.api;
|
||||
import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource;
|
||||
import org.eclipse.jetty.http.HttpVersion;
|
||||
import org.eclipse.jetty.rewrite.handler.RewriteHandler;
|
||||
import org.eclipse.jetty.rewrite.handler.RewritePatternRule;
|
||||
import org.eclipse.jetty.server.*;
|
||||
import org.eclipse.jetty.server.handler.ErrorHandler;
|
||||
import org.eclipse.jetty.server.handler.InetAccessHandler;
|
||||
@@ -16,6 +15,7 @@ import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
@@ -38,7 +38,7 @@ public class DomainMapService {
|
||||
|
||||
private DomainMapService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.domainmap.resource");
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.domainmap.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
@@ -99,13 +99,13 @@ public class DomainMapService {
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getDomainMapPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getDomainMapPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import org.glassfish.jersey.server.ResourceConfig;
|
||||
import org.glassfish.jersey.servlet.ServletContainer;
|
||||
import org.qortal.api.resource.AnnotationPostProcessor;
|
||||
import org.qortal.api.resource.ApiDefinition;
|
||||
import org.qortal.network.Network;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
@@ -37,7 +38,7 @@ public class GatewayService {
|
||||
|
||||
private GatewayService() {
|
||||
this.config = new ResourceConfig();
|
||||
this.config.packages("org.qortal.api.gateway.resource");
|
||||
this.config.packages("org.qortal.api.resource", "org.qortal.api.gateway.resource");
|
||||
this.config.register(OpenApiResource.class);
|
||||
this.config.register(ApiDefinition.class);
|
||||
this.config.register(AnnotationPostProcessor.class);
|
||||
@@ -98,13 +99,13 @@ public class GatewayService {
|
||||
ServerConnector portUnifiedConnector = new ServerConnector(this.server,
|
||||
new DetectorConnectionFactory(sslConnectionFactory),
|
||||
httpConnectionFactory);
|
||||
portUnifiedConnector.setHost(Settings.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setHost(Network.getInstance().getBindAddress());
|
||||
portUnifiedConnector.setPort(Settings.getInstance().getGatewayPort());
|
||||
|
||||
this.server.addConnector(portUnifiedConnector);
|
||||
} else {
|
||||
// Non-SSL
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetAddress bindAddr = InetAddress.getByName(Network.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, Settings.getInstance().getGatewayPort());
|
||||
this.server = new Server(endpoint);
|
||||
}
|
||||
|
@@ -5,6 +5,9 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.nodes.Document;
|
||||
import org.jsoup.select.Elements;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class HTMLParser {
|
||||
|
||||
@@ -12,21 +15,52 @@ public class HTMLParser {
|
||||
|
||||
private String linkPrefix;
|
||||
private byte[] data;
|
||||
private String qdnContext;
|
||||
private String resourceId;
|
||||
private Service service;
|
||||
private String identifier;
|
||||
private String path;
|
||||
private String theme;
|
||||
|
||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data) {
|
||||
String inPathWithoutFilename = inPath.substring(0, inPath.lastIndexOf('/'));
|
||||
this.linkPrefix = usePrefix ? String.format("%s/%s%s", prefix, resourceId, inPathWithoutFilename) : "";
|
||||
public HTMLParser(String resourceId, String inPath, String prefix, boolean usePrefix, byte[] data,
|
||||
String qdnContext, Service service, String identifier, String theme) {
|
||||
this.linkPrefix = usePrefix ? String.format("%s/%s", prefix, resourceId) : "";
|
||||
this.data = data;
|
||||
this.qdnContext = qdnContext;
|
||||
this.resourceId = resourceId;
|
||||
this.service = service;
|
||||
this.identifier = identifier;
|
||||
this.path = inPath;
|
||||
this.theme = theme;
|
||||
}
|
||||
|
||||
public void addAdditionalHeaderTags() {
|
||||
String fileContents = new String(data);
|
||||
Document document = Jsoup.parse(fileContents);
|
||||
String baseUrl = this.linkPrefix + "/";
|
||||
String baseUrl = this.linkPrefix;
|
||||
Elements head = document.getElementsByTag("head");
|
||||
if (!head.isEmpty()) {
|
||||
// Add q-apps script tag
|
||||
String qAppsScriptElement = String.format("<script src=\"/apps/q-apps.js?time=%d\">", System.currentTimeMillis());
|
||||
head.get(0).prepend(qAppsScriptElement);
|
||||
|
||||
// Add q-apps gateway script tag if in gateway mode
|
||||
if (Objects.equals(this.qdnContext, "gateway")) {
|
||||
String qAppsGatewayScriptElement = String.format("<script src=\"/apps/q-apps-gateway.js?time=%d\">", System.currentTimeMillis());
|
||||
head.get(0).prepend(qAppsGatewayScriptElement);
|
||||
}
|
||||
|
||||
// Escape and add vars
|
||||
String service = this.service.toString().replace("\"","\\\"");
|
||||
String name = this.resourceId != null ? this.resourceId.replace("\"","\\\"") : "";
|
||||
String identifier = this.identifier != null ? this.identifier.replace("\"","\\\"") : "";
|
||||
String path = this.path != null ? this.path.replace("\"","\\\"") : "";
|
||||
String theme = this.theme != null ? this.theme.replace("\"","\\\"") : "";
|
||||
String qdnContextVar = String.format("<script>var _qdnContext=\"%s\"; var _qdnTheme=\"%s\"; var _qdnService=\"%s\"; var _qdnName=\"%s\"; var _qdnIdentifier=\"%s\"; var _qdnPath=\"%s\"; var _qdnBase=\"%s\";</script>", this.qdnContext, theme, service, name, identifier, path, baseUrl);
|
||||
head.get(0).prepend(qdnContextVar);
|
||||
|
||||
// Add base href tag
|
||||
String baseElement = String.format("<base href=\"%s\">", baseUrl);
|
||||
String baseElement = String.format("<base href=\"%s/\">", baseUrl);
|
||||
head.get(0).prepend(baseElement);
|
||||
|
||||
// Add meta charset tag
|
||||
|
@@ -15,7 +15,21 @@ public abstract class Security {
|
||||
|
||||
public static final String API_KEY_HEADER = "X-API-KEY";
|
||||
|
||||
/**
|
||||
* Check API call is allowed, retrieving the API key from the request header or GET/POST parameters where required
|
||||
* @param request
|
||||
*/
|
||||
public static void checkApiCallAllowed(HttpServletRequest request) {
|
||||
checkApiCallAllowed(request, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check API call is allowed, retrieving the API key first from the passedApiKey parameter, with a fallback
|
||||
* to the request header or GET/POST parameters when null.
|
||||
* @param request
|
||||
* @param passedApiKey - the API key to test, or null if it should be retrieved from the request headers.
|
||||
*/
|
||||
public static void checkApiCallAllowed(HttpServletRequest request, String passedApiKey) {
|
||||
// We may want to allow automatic authentication for local requests, if enabled in settings
|
||||
boolean localAuthBypassEnabled = Settings.getInstance().isLocalAuthBypassEnabled();
|
||||
if (localAuthBypassEnabled) {
|
||||
@@ -38,7 +52,10 @@ public abstract class Security {
|
||||
}
|
||||
|
||||
// We require an API key to be passed
|
||||
String passedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
if (passedApiKey == null) {
|
||||
// API call not passed as a parameter, so try the header
|
||||
passedApiKey = request.getHeader(API_KEY_HEADER);
|
||||
}
|
||||
if (passedApiKey == null) {
|
||||
// Try query string - this is needed to avoid a CORS preflight. See: https://stackoverflow.com/a/43881141
|
||||
passedApiKey = request.getParameter("apiKey");
|
||||
@@ -56,7 +73,7 @@ public abstract class Security {
|
||||
public static void disallowLoopbackRequests(HttpServletRequest request) {
|
||||
try {
|
||||
InetAddress remoteAddr = InetAddress.getByName(request.getRemoteAddr());
|
||||
if (remoteAddr.isLoopbackAddress()) {
|
||||
if (remoteAddr.isLoopbackAddress() && !Settings.getInstance().isGatewayLoopbackEnabled()) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.UNAUTHORIZED, "Local requests not allowed");
|
||||
}
|
||||
} catch (UnknownHostException e) {
|
||||
@@ -84,9 +101,9 @@ public abstract class Security {
|
||||
}
|
||||
}
|
||||
|
||||
public static void requirePriorAuthorizationOrApiKey(HttpServletRequest request, String resourceId, Service service, String identifier) {
|
||||
public static void requirePriorAuthorizationOrApiKey(HttpServletRequest request, String resourceId, Service service, String identifier, String apiKey) {
|
||||
try {
|
||||
Security.checkApiCallAllowed(request);
|
||||
Security.checkApiCallAllowed(request, apiKey);
|
||||
|
||||
} catch (ApiException e) {
|
||||
// API call wasn't allowed, but maybe it was pre-authorized
|
||||
|
@@ -42,16 +42,16 @@ public class DomainMapResource {
|
||||
// Build synchronously, so that we don't need to make the summary API endpoints available over
|
||||
// the domain map server. This means that there will be no loading screen, but this is potentially
|
||||
// preferred in this situation anyway (e.g. to avoid confusing search engine robots).
|
||||
return this.get(domainMap.get(request.getServerName()), ResourceIdType.NAME, Service.WEBSITE, inPath, null, "", false, false);
|
||||
return this.get(domainMap.get(request.getServerName()), ResourceIdType.NAME, Service.WEBSITE, null, inPath, null, "", false, false);
|
||||
}
|
||||
return ArbitraryDataRenderer.getResponse(response, 404, "Error 404: File Not Found");
|
||||
}
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
||||
String inPath, String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||
secret58, prefix, usePrefix, async, request, response, context);
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
|
||||
secret58, prefix, usePrefix, async, "domainMap", request, response, context);
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.api.gateway.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
@@ -16,6 +17,10 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@Path("/")
|
||||
@@ -76,50 +81,83 @@ public class GatewayResource {
|
||||
|
||||
|
||||
@GET
|
||||
@Path("{name}/{path:.*}")
|
||||
@Path("{path:.*}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathByName(@PathParam("name") String name,
|
||||
@PathParam("path") String inPath) {
|
||||
public HttpServletResponse getPath(@PathParam("path") String inPath) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, inPath, null, "", true, true);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{name}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByName(@PathParam("name") String name) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, "/", null, "", true, true);
|
||||
}
|
||||
|
||||
|
||||
// Optional /site alternative for backwards support
|
||||
|
||||
@GET
|
||||
@Path("/site/{name}/{path:.*}")
|
||||
public HttpServletResponse getSitePathByName(@PathParam("name") String name,
|
||||
@PathParam("path") String inPath) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, inPath, null, "/site", true, true);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/site/{name}")
|
||||
public HttpServletResponse getSiteIndexByName(@PathParam("name") String name) {
|
||||
// Block requests from localhost, to prevent websites/apps from running javascript that fetches unvetted data
|
||||
Security.disallowLoopbackRequests(request);
|
||||
return this.get(name, ResourceIdType.NAME, Service.WEBSITE, "/", null, "/site", true, true);
|
||||
return this.parsePath(inPath, "gateway", null, true, true);
|
||||
}
|
||||
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async) {
|
||||
private HttpServletResponse parsePath(String inPath, String qdnContext, String secret58, boolean usePrefix, boolean async) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||
secret58, prefix, usePrefix, async, request, response, context);
|
||||
if (inPath == null || inPath.equals("")) {
|
||||
// Assume not a real file
|
||||
return ArbitraryDataRenderer.getResponse(response, 404, "Error 404: File Not Found");
|
||||
}
|
||||
|
||||
// Default service is WEBSITE
|
||||
Service service = Service.WEBSITE;
|
||||
String name = null;
|
||||
String identifier = null;
|
||||
String outPath = "";
|
||||
List<String> prefixParts = new ArrayList<>();
|
||||
|
||||
if (!inPath.contains("/")) {
|
||||
// Assume entire inPath is a registered name
|
||||
name = inPath;
|
||||
}
|
||||
else {
|
||||
// Parse the path to determine what we need to load
|
||||
List<String> parts = new LinkedList<>(Arrays.asList(inPath.split("/")));
|
||||
|
||||
// Check if the first element is a service
|
||||
try {
|
||||
Service parsedService = Service.valueOf(parts.get(0).toUpperCase());
|
||||
if (parsedService != null) {
|
||||
// First element matches a service, so we can assume it is one
|
||||
service = parsedService;
|
||||
parts.remove(0);
|
||||
prefixParts.add(service.name());
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Not a service
|
||||
}
|
||||
|
||||
if (parts.isEmpty()) {
|
||||
// We need more than just a service
|
||||
return ArbitraryDataRenderer.getResponse(response, 404, "Error 404: File Not Found");
|
||||
}
|
||||
|
||||
// Service is removed, so assume first element is now a registered name
|
||||
name = parts.get(0);
|
||||
parts.remove(0);
|
||||
|
||||
if (!parts.isEmpty()) {
|
||||
// Name is removed, so check if the first element is now an identifier
|
||||
ArbitraryResourceStatus status = this.getStatus(service, name, parts.get(0), false);
|
||||
if (status.getTotalChunkCount() > 0) {
|
||||
// Matched service, name and identifier combination - so assume this is an identifier and can be removed
|
||||
identifier = parts.get(0);
|
||||
parts.remove(0);
|
||||
prefixParts.add(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
if (!parts.isEmpty()) {
|
||||
// outPath can be built by combining any remaining parts
|
||||
outPath = String.join("/", parts);
|
||||
}
|
||||
}
|
||||
|
||||
String prefix = StringUtils.join(prefixParts, "/");
|
||||
if (prefix != null && prefix.length() > 0) {
|
||||
prefix = "/" + prefix;
|
||||
}
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(name, ResourceIdType.NAME, service, identifier, outPath,
|
||||
secret58, prefix, usePrefix, async, qdnContext, request, response, context);
|
||||
return renderer.render();
|
||||
}
|
||||
|
||||
|
16
src/main/java/org/qortal/api/model/FileProperties.java
Normal file
16
src/main/java/org/qortal/api/model/FileProperties.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package org.qortal.api.model;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class FileProperties {
|
||||
|
||||
public String filename;
|
||||
public String mimeType;
|
||||
public Long size;
|
||||
|
||||
public FileProperties() {
|
||||
}
|
||||
|
||||
}
|
83
src/main/java/org/qortal/api/resource/AppsResource.java
Normal file
83
src/main/java/org/qortal/api/resource/AppsResource.java
Normal file
@@ -0,0 +1,83 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.*;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
|
||||
@Path("/apps")
|
||||
@Tag(name = "Apps")
|
||||
public class AppsResource {
|
||||
|
||||
@Context HttpServletRequest request;
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
@GET
|
||||
@Path("/q-apps.js")
|
||||
@Hidden // For internal Q-App API use only
|
||||
@Operation(
|
||||
summary = "Javascript interface for Q-Apps",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "javascript",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getQAppsJs() {
|
||||
URL url = Resources.getResource("q-apps/q-apps.js");
|
||||
try {
|
||||
return Resources.toString(url, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/q-apps-gateway.js")
|
||||
@Hidden // For internal Q-App API use only
|
||||
@Operation(
|
||||
summary = "Gateway-specific interface for Q-Apps",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "javascript",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
public String getQAppsGatewayJs() {
|
||||
URL url = Resources.getResource("q-apps/q-apps-gateway.js");
|
||||
try {
|
||||
return Resources.toString(url, StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,8 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.j256.simplemagic.ContentInfo;
|
||||
import com.j256.simplemagic.ContentInfoUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
@@ -12,11 +14,14 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.FileNameMap;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -25,11 +30,13 @@ import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.FileProperties;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.arbitrary.*;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.ResourceIdType;
|
||||
@@ -38,6 +45,7 @@ import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataRenderManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryMetadataManager;
|
||||
import org.qortal.data.account.AccountData;
|
||||
@@ -88,12 +96,15 @@ public class ArbitraryResource {
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceInfo> getResources(
|
||||
@QueryParam("service") Service service,
|
||||
@QueryParam("name") String name,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||
@Parameter(description = "Filter names by list") @QueryParam("namefilter") String nameFilter,
|
||||
@Parameter(description = "Include followed names only") @QueryParam("followedonly") Boolean followedOnly,
|
||||
@Parameter(description = "Exclude blocked content") @QueryParam("excludeblocked") Boolean excludeBlocked,
|
||||
@Parameter(description = "Filter names by list") @QueryParam("namefilter") String nameListFilter,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||
|
||||
@@ -110,28 +121,33 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
|
||||
}
|
||||
|
||||
// Load filter from list if needed
|
||||
// Set up name filters if supplied
|
||||
List<String> names = null;
|
||||
if (nameFilter != null) {
|
||||
names = ResourceListManager.getInstance().getStringsInList(nameFilter);
|
||||
if (name != null) {
|
||||
// Filter using single name
|
||||
names = Arrays.asList(name);
|
||||
}
|
||||
else if (nameListFilter != null) {
|
||||
// Filter using supplied list of names
|
||||
names = ResourceListManager.getInstance().getStringsInList(nameListFilter);
|
||||
if (names.isEmpty()) {
|
||||
// List doesn't exist or is empty - so there will be no matches
|
||||
// If list is empty (or doesn't exist) we can shortcut with empty response
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse);
|
||||
.getArbitraryResources(service, identifier, names, defaultRes, followedOnly, excludeBlocked, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@@ -155,30 +171,49 @@ public class ArbitraryResource {
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceInfo> searchResources(
|
||||
@QueryParam("service") Service service,
|
||||
@QueryParam("query") String query,
|
||||
@Parameter(description = "Query (searches both name and identifier fields)") @QueryParam("query") String query,
|
||||
@Parameter(description = "Identifier (searches identifier field only)") @QueryParam("identifier") String identifier,
|
||||
@Parameter(description = "Name (searches name field only)") @QueryParam("name") List<String> names,
|
||||
@Parameter(description = "Prefix only (if true, only the beginning of fields are matched)") @QueryParam("prefix") Boolean prefixOnly,
|
||||
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
|
||||
@Parameter(description = "Filter names by list (exact matches only)") @QueryParam("namefilter") String nameListFilter,
|
||||
@Parameter(description = "Include followed names only") @QueryParam("followedonly") Boolean followedOnly,
|
||||
@Parameter(description = "Exclude blocked content") @QueryParam("excludeblocked") Boolean excludeBlocked,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
|
||||
boolean usePrefixOnly = Boolean.TRUE.equals(prefixOnly);
|
||||
|
||||
List<String> exactMatchNames = new ArrayList<>();
|
||||
|
||||
if (nameListFilter != null) {
|
||||
// Load names from supplied list of names
|
||||
exactMatchNames.addAll(ResourceListManager.getInstance().getStringsInList(nameListFilter));
|
||||
|
||||
// If list is empty (or doesn't exist) we can shortcut with empty response
|
||||
if (exactMatchNames.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.searchArbitraryResources(service, query, defaultRes, limit, offset, reverse);
|
||||
.searchArbitraryResources(service, query, identifier, names, usePrefixOnly, exactMatchNames, defaultRes, followedOnly, excludeBlocked, limit, offset, reverse);
|
||||
|
||||
if (resources == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@@ -188,67 +223,6 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resources/names")
|
||||
@Operation(
|
||||
summary = "List arbitrary resources available on chain, grouped by creator's name",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = ArbitraryResourceInfo.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<ArbitraryResourceNameInfo> getResourcesGroupedByName(
|
||||
@QueryParam("service") Service service,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@Parameter(description = "Default resources (without identifiers) only") @QueryParam("default") Boolean defaultResource,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse,
|
||||
@Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus,
|
||||
@Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
// Treat empty identifier as null
|
||||
if (identifier != null && identifier.isEmpty()) {
|
||||
identifier = null;
|
||||
}
|
||||
|
||||
// Ensure that "default" and "identifier" parameters cannot coexist
|
||||
boolean defaultRes = Boolean.TRUE.equals(defaultResource);
|
||||
if (defaultRes == true && identifier != null) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource");
|
||||
}
|
||||
|
||||
List<ArbitraryResourceNameInfo> creatorNames = repository.getArbitraryRepository()
|
||||
.getArbitraryResourceCreatorNames(service, identifier, defaultRes, limit, offset, reverse);
|
||||
|
||||
for (ArbitraryResourceNameInfo creatorName : creatorNames) {
|
||||
String name = creatorName.name;
|
||||
if (name != null) {
|
||||
List<ArbitraryResourceInfo> resources = repository.getArbitraryRepository()
|
||||
.getArbitraryResources(service, identifier, Arrays.asList(name), defaultRes, null, null, reverse);
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
creatorName.resources = resources;
|
||||
}
|
||||
}
|
||||
|
||||
return creatorNames;
|
||||
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resource/status/{service}/{name}")
|
||||
@Operation(
|
||||
@@ -266,10 +240,35 @@ public class ArbitraryResource {
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, null, apiKey);
|
||||
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, null, build);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resource/properties/{service}/{name}/{identifier}")
|
||||
@Operation(
|
||||
summary = "Get properties of a QDN resource",
|
||||
description = "This attempts a download of the data if it's not available locally. A filename will only be returned for single file resources. mimeType is only returned when it can be determined.",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = FileProperties.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public FileProperties getResourceProperties(@HeaderParam(Security.API_KEY_HEADER) String apiKey,
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier) {
|
||||
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier, apiKey);
|
||||
|
||||
return this.getFileProperties(service, name, identifier);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/resource/status/{service}/{name}/{identifier}")
|
||||
@Operation(
|
||||
@@ -288,7 +287,9 @@ public class ArbitraryResource {
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("build") Boolean build) {
|
||||
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorizationOrApiKey(request, name, service, identifier, apiKey);
|
||||
|
||||
return ArbitraryTransactionUtils.getStatus(service, name, identifier, build);
|
||||
}
|
||||
|
||||
@@ -501,6 +502,9 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||
if (transactionData.getService() == null) {
|
||||
continue;
|
||||
}
|
||||
ArbitraryResourceInfo arbitraryResourceInfo = new ArbitraryResourceInfo();
|
||||
arbitraryResourceInfo.name = transactionData.getName();
|
||||
arbitraryResourceInfo.service = transactionData.getService();
|
||||
@@ -511,10 +515,10 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
if (includeStatus != null && includeStatus) {
|
||||
resources = this.addStatusToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addStatusToResources(resources);
|
||||
}
|
||||
if (includeMetadata != null && includeMetadata) {
|
||||
resources = this.addMetadataToResources(resources);
|
||||
resources = ArbitraryTransactionUtils.addMetadataToResources(resources);
|
||||
}
|
||||
|
||||
return resources;
|
||||
@@ -544,7 +548,7 @@ public class ArbitraryResource {
|
||||
|
||||
Security.checkApiCallAllowed(request);
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(name, ResourceIdType.NAME, service, identifier);
|
||||
return resource.delete();
|
||||
return resource.delete(false);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -641,6 +645,7 @@ public class ArbitraryResource {
|
||||
@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("encoding") String encoding,
|
||||
@QueryParam("rebuild") boolean rebuild,
|
||||
@QueryParam("async") boolean async,
|
||||
@QueryParam("attempts") Integer attempts) {
|
||||
@@ -650,7 +655,7 @@ public class ArbitraryResource {
|
||||
Security.checkApiCallAllowed(request);
|
||||
}
|
||||
|
||||
return this.download(service, name, null, filepath, rebuild, async, attempts);
|
||||
return this.download(service, name, null, filepath, encoding, rebuild, async, attempts);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -676,16 +681,17 @@ public class ArbitraryResource {
|
||||
@PathParam("name") String name,
|
||||
@PathParam("identifier") String identifier,
|
||||
@QueryParam("filepath") String filepath,
|
||||
@QueryParam("encoding") String encoding,
|
||||
@QueryParam("rebuild") boolean rebuild,
|
||||
@QueryParam("async") boolean async,
|
||||
@QueryParam("attempts") Integer attempts) {
|
||||
|
||||
// Authentication can be bypassed in the settings, for those running public QDN nodes
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled()) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
Security.checkApiCallAllowed(request, apiKey);
|
||||
}
|
||||
|
||||
return this.download(service, name, identifier, filepath, rebuild, async, attempts);
|
||||
return this.download(service, name, identifier, filepath, encoding, rebuild, async, attempts);
|
||||
}
|
||||
|
||||
|
||||
@@ -733,7 +739,7 @@ public class ArbitraryResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FILE_NOT_FOUND);
|
||||
}
|
||||
|
||||
|
||||
@@ -773,6 +779,8 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String path) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -781,7 +789,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, path, null, null, false,
|
||||
title, description, tags, category);
|
||||
fee, null, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -818,6 +826,8 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String path) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -826,7 +836,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, path, null, null, false,
|
||||
title, description, tags, category);
|
||||
fee, null, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
|
||||
@@ -864,6 +874,9 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("filename") String filename,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String base64) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -872,7 +885,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64, false,
|
||||
title, description, tags, category);
|
||||
fee, filename, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -907,6 +920,9 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("filename") String filename,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String base64) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -915,7 +931,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64, false,
|
||||
title, description, tags, category);
|
||||
fee, filename, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
|
||||
@@ -952,6 +968,8 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String base64Zip) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -960,7 +978,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, null, base64Zip, true,
|
||||
title, description, tags, category);
|
||||
fee, null, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -995,6 +1013,8 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String base64Zip) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1003,7 +1023,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, null, base64Zip, true,
|
||||
title, description, tags, category);
|
||||
fee, null, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
|
||||
@@ -1043,6 +1063,9 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("filename") String filename,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String string) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1051,7 +1074,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, null, null, string, null, false,
|
||||
title, description, tags, category);
|
||||
fee, filename, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
@POST
|
||||
@@ -1088,6 +1111,9 @@ public class ArbitraryResource {
|
||||
@QueryParam("description") String description,
|
||||
@QueryParam("tags") List<String> tags,
|
||||
@QueryParam("category") Category category,
|
||||
@QueryParam("filename") String filename,
|
||||
@QueryParam("fee") Long fee,
|
||||
@QueryParam("preview") Boolean preview,
|
||||
String string) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
@@ -1096,15 +1122,48 @@ public class ArbitraryResource {
|
||||
}
|
||||
|
||||
return this.upload(Service.valueOf(serviceString), name, identifier, null, string, null, false,
|
||||
title, description, tags, category);
|
||||
fee, filename, title, description, tags, category, preview);
|
||||
}
|
||||
|
||||
|
||||
// Shared methods
|
||||
|
||||
private String preview(String directoryPath, Service service) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
ArbitraryTransactionData.Method method = ArbitraryTransactionData.Method.PUT;
|
||||
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.ZIP;
|
||||
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(directoryPath),
|
||||
null, service, null, method, compression,
|
||||
null, null, null, null);
|
||||
try {
|
||||
arbitraryDataWriter.save();
|
||||
} catch (IOException | DataException | InterruptedException | MissingDataException e) {
|
||||
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_DATA, e.getMessage());
|
||||
}
|
||||
|
||||
ArbitraryDataFile arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile();
|
||||
if (arbitraryDataFile != null) {
|
||||
String digest58 = arbitraryDataFile.digest58();
|
||||
if (digest58 != null) {
|
||||
// Pre-authorize resource
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(digest58, null, null, null);
|
||||
ArbitraryDataRenderManager.getInstance().addToAuthorizedResources(resource);
|
||||
|
||||
return "/render/hash/" + digest58 + "?secret=" + Base58.encode(arbitraryDataFile.getSecret());
|
||||
}
|
||||
}
|
||||
return "Unable to generate preview URL";
|
||||
}
|
||||
|
||||
private String upload(Service service, String name, String identifier,
|
||||
String path, String string, String base64, boolean zipped,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
String path, String string, String base64, boolean zipped, Long fee, String filename,
|
||||
String title, String description, List<String> tags, Category category,
|
||||
Boolean preview) {
|
||||
// Fetch public key from registered name
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
NameData nameData = repository.getNameRepository().fromName(name);
|
||||
@@ -1128,7 +1187,12 @@ public class ArbitraryResource {
|
||||
if (path == null) {
|
||||
// See if we have a string instead
|
||||
if (string != null) {
|
||||
File tempFile = File.createTempFile("qortal-", "");
|
||||
if (filename == null) {
|
||||
// Use current time as filename
|
||||
filename = String.format("qortal-%d", NTP.getTime());
|
||||
}
|
||||
java.nio.file.Path tempDirectory = Files.createTempDirectory("qortal-");
|
||||
File tempFile = Paths.get(tempDirectory.toString(), filename).toFile();
|
||||
tempFile.deleteOnExit();
|
||||
BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile.toPath().toString()));
|
||||
writer.write(string);
|
||||
@@ -1138,7 +1202,12 @@ public class ArbitraryResource {
|
||||
}
|
||||
// ... or base64 encoded raw data
|
||||
else if (base64 != null) {
|
||||
File tempFile = File.createTempFile("qortal-", "");
|
||||
if (filename == null) {
|
||||
// Use current time as filename
|
||||
filename = String.format("qortal-%d", NTP.getTime());
|
||||
}
|
||||
java.nio.file.Path tempDirectory = Files.createTempDirectory("qortal-");
|
||||
File tempFile = Paths.get(tempDirectory.toString(), filename).toFile();
|
||||
tempFile.deleteOnExit();
|
||||
Files.write(tempFile.toPath(), Base64.decode(base64));
|
||||
path = tempFile.toPath().toString();
|
||||
@@ -1167,9 +1236,19 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
// Finish here if user has requested a preview
|
||||
if (preview != null && preview == true) {
|
||||
return this.preview(path, service);
|
||||
}
|
||||
|
||||
// Default to zero fee if not specified
|
||||
if (fee == null) {
|
||||
fee = 0L;
|
||||
}
|
||||
|
||||
try {
|
||||
ArbitraryDataTransactionBuilder transactionBuilder = new ArbitraryDataTransactionBuilder(
|
||||
repository, publicKey58, Paths.get(path), name, null, service, identifier,
|
||||
repository, publicKey58, fee, Paths.get(path), name, null, service, identifier,
|
||||
title, description, tags, category
|
||||
);
|
||||
|
||||
@@ -1188,7 +1267,7 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, boolean rebuild, boolean async, Integer maxAttempts) {
|
||||
private HttpServletResponse download(Service service, String name, String identifier, String filepath, String encoding, boolean rebuild, boolean async, Integer maxAttempts) {
|
||||
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
@@ -1247,7 +1326,14 @@ public class ArbitraryResource {
|
||||
String message = String.format("No file exists at filepath: %s", filepath);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, message);
|
||||
}
|
||||
|
||||
byte[] data = Files.readAllBytes(path);
|
||||
|
||||
// Encode the data if requested
|
||||
if (encoding != null && Objects.equals(encoding.toLowerCase(), "base64")) {
|
||||
data = Base64.encode(data);
|
||||
}
|
||||
|
||||
response.setContentType(context.getMimeType(path.toString()));
|
||||
response.setContentLength(data.length);
|
||||
response.getOutputStream().write(data);
|
||||
@@ -1259,41 +1345,44 @@ public class ArbitraryResource {
|
||||
}
|
||||
}
|
||||
|
||||
private FileProperties getFileProperties(Service service, String name, String identifier) {
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
arbitraryDataReader.loadSynchronously(false);
|
||||
java.nio.file.Path outputPath = arbitraryDataReader.getFilePath();
|
||||
if (outputPath == null) {
|
||||
// Assume the resource doesn't exist
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, "File not found");
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
FileProperties fileProperties = new FileProperties();
|
||||
fileProperties.size = FileUtils.sizeOfDirectory(outputPath.toFile());
|
||||
|
||||
String[] files = ArrayUtils.removeElement(outputPath.toFile().list(), ".qortal");
|
||||
if (files.length == 1) {
|
||||
String filename = files[0];
|
||||
java.nio.file.Path filePath = Paths.get(outputPath.toString(), files[0]);
|
||||
ContentInfoUtil util = new ContentInfoUtil();
|
||||
ContentInfo info = util.findMatch(filePath.toFile());
|
||||
String mimeType;
|
||||
if (info != null) {
|
||||
// Attempt to extract MIME type from file contents
|
||||
mimeType = info.getMimeType();
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource %s: %s", resourceInfo, e.toString());
|
||||
else {
|
||||
// Fall back to using the filename
|
||||
FileNameMap fileNameMap = URLConnection.getFileNameMap();
|
||||
mimeType = fileNameMap.getContentTypeFor(filename);
|
||||
}
|
||||
fileProperties.filename = filename;
|
||||
fileProperties.mimeType = mimeType;
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
private List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, false);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
return fileProperties;
|
||||
|
||||
} catch (Exception e) {
|
||||
LOGGER.debug(String.format("Unable to load %s %s: %s", service, name, e.getMessage()));
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FILE_NOT_FOUND, e.getMessage());
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
}
|
||||
|
@@ -48,6 +48,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@Path("/blocks")
|
||||
@Tag(name = "Blocks")
|
||||
@@ -165,10 +166,13 @@ public class BlocksResource {
|
||||
}
|
||||
|
||||
// Not found, so try the block archive
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||
if (bytes != null) {
|
||||
if (version != 1) {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Archived blocks require version 1");
|
||||
Triple<byte[], Integer, Integer> serializedBlock = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, false, repository);
|
||||
if (serializedBlock != null) {
|
||||
byte[] bytes = serializedBlock.getA();
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
if (version != serializationVersion) {
|
||||
// TODO: we could quite easily reserialize the block with the requested version
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Block is not stored using requested serialization version.");
|
||||
}
|
||||
return Base58.encode(bytes);
|
||||
}
|
||||
|
@@ -40,6 +40,8 @@ import org.qortal.utils.Base58;
|
||||
|
||||
import com.google.common.primitives.Bytes;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@Path("/chat")
|
||||
@Tag(name = "Chat")
|
||||
public class ChatResource {
|
||||
@@ -72,6 +74,8 @@ public class ChatResource {
|
||||
@QueryParam("reference") String reference,
|
||||
@QueryParam("chatreference") String chatReference,
|
||||
@QueryParam("haschatreference") Boolean hasChatReference,
|
||||
@QueryParam("sender") String sender,
|
||||
@QueryParam("encoding") Encoding encoding,
|
||||
@Parameter(ref = "limit") @QueryParam("limit") Integer limit,
|
||||
@Parameter(ref = "offset") @QueryParam("offset") Integer offset,
|
||||
@Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse) {
|
||||
@@ -107,6 +111,8 @@ public class ChatResource {
|
||||
chatReferenceBytes,
|
||||
hasChatReference,
|
||||
involvingAddresses,
|
||||
sender,
|
||||
encoding,
|
||||
limit, offset, reverse);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
@@ -129,7 +135,7 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public ChatMessage getMessageBySignature(@PathParam("signature") String signature58) {
|
||||
public ChatMessage getMessageBySignature(@PathParam("signature") String signature58, @QueryParam("encoding") Encoding encoding) {
|
||||
byte[] signature = Base58.decode(signature58);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
@@ -139,7 +145,7 @@ public class ChatResource {
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Message not found");
|
||||
}
|
||||
|
||||
return repository.getChatRepository().toChatMessage(chatTransactionData);
|
||||
return repository.getChatRepository().toChatMessage(chatTransactionData, encoding);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
@@ -162,12 +168,12 @@ public class ChatResource {
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
|
||||
public ActiveChats getActiveChats(@PathParam("address") String address) {
|
||||
public ActiveChats getActiveChats(@PathParam("address") String address, @QueryParam("encoding") Encoding encoding) {
|
||||
if (address == null || !Crypto.isValidAddress(address))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
return repository.getChatRepository().getActiveChats(address);
|
||||
return repository.getChatRepository().getActiveChats(address, encoding);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
|
197
src/main/java/org/qortal/api/resource/PollsResource.java
Normal file
197
src/main/java/org/qortal/api/resource/PollsResource.java
Normal file
@@ -0,0 +1,197 @@
|
||||
package org.qortal.api.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.data.transaction.CreatePollTransactionData;
|
||||
import org.qortal.data.transaction.PaymentTransactionData;
|
||||
import org.qortal.data.transaction.VoteOnPollTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.CreatePollTransactionTransformer;
|
||||
import org.qortal.transform.transaction.PaymentTransactionTransformer;
|
||||
import org.qortal.transform.transaction.VoteOnPollTransactionTransformer;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import java.util.List;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.data.voting.PollData;
|
||||
|
||||
@Path("/polls")
|
||||
@Tag(name = "Polls")
|
||||
public class PollsResource {
|
||||
@Context
|
||||
HttpServletRequest request;
|
||||
|
||||
@GET
|
||||
@Operation(
|
||||
summary = "List all polls",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "poll info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
array = @ArraySchema(schema = @Schema(implementation = PollData.class))
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public List<PollData> getAllPolls(@Parameter(
|
||||
ref = "limit"
|
||||
) @QueryParam("limit") Integer limit, @Parameter(
|
||||
ref = "offset"
|
||||
) @QueryParam("offset") Integer offset, @Parameter(
|
||||
ref = "reverse"
|
||||
) @QueryParam("reverse") Boolean reverse) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<PollData> allPollData = repository.getVotingRepository().getAllPolls(limit, offset, reverse);
|
||||
return allPollData;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/{pollName}")
|
||||
@Operation(
|
||||
summary = "Info on poll",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "poll info",
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(implementation = PollData.class)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
public PollData getPollData(@PathParam("pollName") String pollName) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
PollData pollData = repository.getVotingRepository().fromPollName(pollName);
|
||||
if (pollData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.POLL_NO_EXISTS);
|
||||
|
||||
return pollData;
|
||||
} catch (ApiException e) {
|
||||
throw e;
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/create")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, CREATE_POLL transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = CreatePollTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, CREATE_POLL transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String CreatePoll(CreatePollTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != Transaction.ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = CreatePollTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/vote")
|
||||
@Operation(
|
||||
summary = "Build raw, unsigned, VOTE_ON_POLL transaction",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = VoteOnPollTransactionData.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "raw, unsigned, VOTE_ON_POLL transaction encoded in Base58",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.NON_PRODUCTION, ApiError.TRANSACTION_INVALID, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
|
||||
public String VoteOnPoll(VoteOnPollTransactionData transactionData) {
|
||||
if (Settings.getInstance().isApiRestricted())
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.NON_PRODUCTION);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
Transaction transaction = Transaction.fromData(repository, transactionData);
|
||||
|
||||
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != Transaction.ValidationResult.OK)
|
||||
throw TransactionsResource.createTransactionInvalidException(request, result);
|
||||
|
||||
byte[] bytes = VoteOnPollTransactionTransformer.toBytes(transactionData);
|
||||
return Base58.encode(bytes);
|
||||
} catch (TransformationException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -9,6 +9,8 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.ArrayList;
|
||||
@@ -18,19 +20,12 @@ import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.ApiError;
|
||||
import org.qortal.api.ApiErrors;
|
||||
import org.qortal.api.ApiException;
|
||||
import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.*;
|
||||
import org.qortal.api.model.SimpleTransactionSignRequest;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.LiteNode;
|
||||
@@ -709,7 +704,7 @@ public class TransactionsResource {
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "true if accepted, false otherwise",
|
||||
description = "For API version 1, this returns true if accepted.\nFor API version 2, the transactionData is returned as a JSON string if accepted.",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
@@ -722,7 +717,9 @@ public class TransactionsResource {
|
||||
@ApiErrors({
|
||||
ApiError.BLOCKCHAIN_NEEDS_SYNC, ApiError.INVALID_SIGNATURE, ApiError.INVALID_DATA, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE
|
||||
})
|
||||
public String processTransaction(String rawBytes58) {
|
||||
public String processTransaction(String rawBytes58, @HeaderParam(ApiService.API_VERSION_HEADER) String apiVersionHeader) {
|
||||
int apiVersion = ApiService.getApiVersion(request);
|
||||
|
||||
// Only allow a transaction to be processed if our latest block is less than 60 minutes old
|
||||
// If older than this, we should first wait until the blockchain is synced
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
@@ -759,13 +756,27 @@ public class TransactionsResource {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
|
||||
return "true";
|
||||
switch (apiVersion) {
|
||||
case 1:
|
||||
return "true";
|
||||
|
||||
case 2:
|
||||
default:
|
||||
// Marshall transactionData to string
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
ApiRequest.marshall(stringWriter, transactionData);
|
||||
return stringWriter.toString();
|
||||
}
|
||||
|
||||
|
||||
} catch (NumberFormatException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA, e);
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
} catch (InterruptedException e) {
|
||||
throw createTransactionInvalidException(request, ValidationResult.NO_BLOCKCHAIN_LOCK);
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
package org.qortal.api.resource;
|
||||
package org.qortal.api.restricted.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
@@ -20,6 +20,7 @@ import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -31,10 +32,13 @@ import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.apache.logging.log4j.core.LoggerContext;
|
||||
import org.apache.logging.log4j.core.appender.RollingFileAppender;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.api.*;
|
||||
@@ -42,9 +46,11 @@ import org.qortal.api.model.ActivitySummary;
|
||||
import org.qortal.api.model.NodeInfo;
|
||||
import org.qortal.api.model.NodeStatus;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.AutoUpdate;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||
import org.qortal.controller.repository.BlockArchiveRebuilder;
|
||||
import org.qortal.data.account.MintingAccountData;
|
||||
import org.qortal.data.account.RewardShareData;
|
||||
import org.qortal.network.Network;
|
||||
@@ -152,6 +158,53 @@ public class AdminResource {
|
||||
return nodeStatus;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/settings")
|
||||
@Operation(
|
||||
summary = "Fetch node settings",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = Settings.class))
|
||||
)
|
||||
}
|
||||
)
|
||||
public Settings settings() {
|
||||
Settings nodeSettings = Settings.getInstance();
|
||||
|
||||
return nodeSettings;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/settings/{setting}")
|
||||
@Operation(
|
||||
summary = "Fetch a single node setting",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
public Object setting(@PathParam("setting") String setting) {
|
||||
try {
|
||||
Object settingValue = FieldUtils.readField(Settings.getInstance(), setting, true);
|
||||
if (settingValue == null) {
|
||||
return "null";
|
||||
}
|
||||
else if (settingValue instanceof String[]) {
|
||||
JSONArray array = new JSONArray(settingValue);
|
||||
return array.toString(4);
|
||||
}
|
||||
else if (settingValue instanceof List) {
|
||||
JSONArray array = new JSONArray((List<Object>) settingValue);
|
||||
return array.toString(4);
|
||||
}
|
||||
return settingValue;
|
||||
|
||||
} catch (IllegalAccessException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/stop")
|
||||
@Operation(
|
||||
@@ -182,6 +235,37 @@ public class AdminResource {
|
||||
return "true";
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/restart")
|
||||
@Operation(
|
||||
summary = "Restart",
|
||||
description = "Restart",
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String restart(@HeaderParam(Security.API_KEY_HEADER) String apiKey) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
new Thread(() -> {
|
||||
// Short sleep to allow HTTP response body to be emitted
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
} catch (InterruptedException e) {
|
||||
// Not important
|
||||
}
|
||||
|
||||
AutoUpdate.attemptRestart();
|
||||
|
||||
}).start();
|
||||
|
||||
return "true";
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/summary")
|
||||
@Operation(
|
||||
@@ -734,6 +818,64 @@ public class AdminResource {
|
||||
}
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/repository/archive/rebuild")
|
||||
@Operation(
|
||||
summary = "Rebuild archive",
|
||||
description = "Rebuilds archive files, using the specified serialization version",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "number", example = "2"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "\"true\"",
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.REPOSITORY_ISSUE})
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String rebuildArchive(@HeaderParam(Security.API_KEY_HEADER) String apiKey, Integer serializationVersion) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
// Default serialization version to value specified in settings
|
||||
if (serializationVersion == null) {
|
||||
serializationVersion = Settings.getInstance().getDefaultArchiveVersion();
|
||||
}
|
||||
|
||||
try {
|
||||
// We don't actually need to lock the blockchain here, but we'll do it anyway so that
|
||||
// the node can focus on rebuilding rather than synchronizing / minting.
|
||||
ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock();
|
||||
|
||||
blockchainLock.lockInterruptibly();
|
||||
|
||||
try {
|
||||
BlockArchiveRebuilder blockArchiveRebuilder = new BlockArchiveRebuilder(serializationVersion);
|
||||
blockArchiveRebuilder.start();
|
||||
|
||||
return "true";
|
||||
|
||||
} catch (IOException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e);
|
||||
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// We couldn't lock blockchain to perform rebuild
|
||||
return "false";
|
||||
} catch (DataException e) {
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||
}
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("/repository")
|
||||
@Operation(
|
@@ -1,4 +1,4 @@
|
||||
package org.qortal.api.resource;
|
||||
package org.qortal.api.restricted.resource;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
@@ -60,7 +60,7 @@ public class BootstrapResource {
|
||||
bootstrap.validateBlockchain();
|
||||
return bootstrap.create();
|
||||
|
||||
} catch (DataException | InterruptedException | IOException e) {
|
||||
} catch (Exception e) {
|
||||
LOGGER.info("Unable to create bootstrap", e);
|
||||
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package org.qortal.api.resource;
|
||||
package org.qortal.api.restricted.resource;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -8,7 +8,6 @@ import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.io.*;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Map;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.media.Content;
|
||||
@@ -28,8 +27,8 @@ import org.qortal.arbitrary.exception.MissingDataException;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataRenderManager;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
|
||||
@@ -43,60 +42,6 @@ public class RenderResource {
|
||||
@Context HttpServletResponse response;
|
||||
@Context ServletContext context;
|
||||
|
||||
@POST
|
||||
@Path("/preview")
|
||||
@Operation(
|
||||
summary = "Generate preview URL based on a user-supplied path and service",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string", example = "/Users/user/Documents/MyStaticWebsite"
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
description = "a temporary URL to preview the website",
|
||||
content = @Content(
|
||||
mediaType = MediaType.TEXT_PLAIN,
|
||||
schema = @Schema(
|
||||
type = "string"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String preview(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String directoryPath) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
Method method = Method.PUT;
|
||||
Compression compression = Compression.ZIP;
|
||||
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(Paths.get(directoryPath),
|
||||
null, Service.WEBSITE, null, method, compression,
|
||||
null, null, null, null);
|
||||
try {
|
||||
arbitraryDataWriter.save();
|
||||
} catch (IOException | DataException | InterruptedException | MissingDataException e) {
|
||||
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE);
|
||||
} catch (RuntimeException e) {
|
||||
LOGGER.info("Unable to create arbitrary data file: {}", e.getMessage());
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
|
||||
}
|
||||
|
||||
ArbitraryDataFile arbitraryDataFile = arbitraryDataWriter.getArbitraryDataFile();
|
||||
if (arbitraryDataFile != null) {
|
||||
String digest58 = arbitraryDataFile.digest58();
|
||||
if (digest58 != null) {
|
||||
return "http://localhost:12393/render/hash/" + digest58 + "?secret=" + Base58.encode(arbitraryDataFile.getSecret());
|
||||
}
|
||||
}
|
||||
return "Unable to generate preview URL";
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/authorize/{resourceId}")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
@@ -140,8 +85,10 @@ public class RenderResource {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexBySignature(@PathParam("signature") String signature,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, "/", null, "/render/signature", true, true, theme);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, "/", null, "/render/signature", true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -149,8 +96,10 @@ public class RenderResource {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getPathBySignature(@PathParam("signature") String signature, @PathParam("path") String inPath,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, inPath,null, "/render/signature", true, true, theme);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, signature, Service.WEBSITE, null);
|
||||
|
||||
return this.get(signature, ResourceIdType.SIGNATURE, null, null, inPath,null, "/render/signature", true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -158,8 +107,10 @@ public class RenderResource {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByHash(@PathParam("hash") String hash58, @QueryParam("secret") String secret58,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.WEBSITE, "/", secret58, "/render/hash", true, false, theme);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, "/", secret58, "/render/hash", true, false, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -168,8 +119,10 @@ public class RenderResource {
|
||||
public HttpServletResponse getPathByHash(@PathParam("hash") String hash58, @PathParam("path") String inPath,
|
||||
@QueryParam("secret") String secret58,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.WEBSITE, inPath, secret58, "/render/hash", true, false, theme);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, hash58, Service.WEBSITE, null);
|
||||
|
||||
return this.get(hash58, ResourceIdType.FILE_HASH, Service.ARBITRARY_DATA, null, inPath, secret58, "/render/hash", true, false, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -178,10 +131,13 @@ public class RenderResource {
|
||||
public HttpServletResponse getPathByName(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@PathParam("path") String inPath,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
|
||||
String prefix = String.format("/render/%s", service);
|
||||
return this.get(name, ResourceIdType.NAME, service, inPath, null, prefix, true, true, theme);
|
||||
return this.get(name, ResourceIdType.NAME, service, identifier, inPath, null, prefix, true, true, theme);
|
||||
}
|
||||
|
||||
@GET
|
||||
@@ -189,19 +145,22 @@ public class RenderResource {
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public HttpServletResponse getIndexByName(@PathParam("service") Service service,
|
||||
@PathParam("name") String name,
|
||||
@QueryParam("identifier") String identifier,
|
||||
@QueryParam("theme") String theme) {
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
if (!Settings.getInstance().isQDNAuthBypassEnabled())
|
||||
Security.requirePriorAuthorization(request, name, service, null);
|
||||
|
||||
String prefix = String.format("/render/%s", service);
|
||||
return this.get(name, ResourceIdType.NAME, service, "/", null, prefix, true, true, theme);
|
||||
return this.get(name, ResourceIdType.NAME, service, identifier, "/", null, prefix, true, true, theme);
|
||||
}
|
||||
|
||||
|
||||
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async, String theme) {
|
||||
private HttpServletResponse get(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
||||
String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String theme) {
|
||||
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, inPath,
|
||||
secret58, prefix, usePrefix, async, request, response, context);
|
||||
ArbitraryDataRenderer renderer = new ArbitraryDataRenderer(resourceId, resourceIdType, service, identifier, inPath,
|
||||
secret58, prefix, usePrefix, async, "render", request, response, context);
|
||||
|
||||
if (theme != null) {
|
||||
renderer.setTheme(theme);
|
@@ -2,7 +2,9 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
@@ -21,6 +23,8 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
@@ -62,7 +66,9 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
if (Objects.equals(message, "ping")) {
|
||||
session.getRemote().sendStringByFuture("pong");
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, String ourAddress, AtomicReference<String> previousOutput) {
|
||||
@@ -75,7 +81,7 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
}
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress);
|
||||
ActiveChats activeChats = repository.getChatRepository().getActiveChats(ourAddress, getTargetEncoding(session));
|
||||
|
||||
StringWriter stringWriter = new StringWriter();
|
||||
|
||||
@@ -93,4 +99,12 @@ public class ActiveChatsWebSocket extends ApiWebSocket {
|
||||
}
|
||||
}
|
||||
|
||||
private Encoding getTargetEncoding(Session session) {
|
||||
// Default to Base58 if not specified, for backwards support
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
List<String> encodingList = queryParams.get("encoding");
|
||||
String encoding = (encodingList != null && encodingList.size() == 1) ? encodingList.get(0) : "BASE58";
|
||||
return Encoding.valueOf(encoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -2,10 +2,7 @@ package org.qortal.api.websocket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
import org.eclipse.jetty.websocket.api.Session;
|
||||
import org.eclipse.jetty.websocket.api.WebSocketException;
|
||||
@@ -22,6 +19,8 @@ import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@WebSocket
|
||||
@SuppressWarnings("serial")
|
||||
public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
@@ -35,6 +34,16 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
@Override
|
||||
public void onWebSocketConnect(Session session) {
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
Encoding encoding = getTargetEncoding(session);
|
||||
|
||||
List<String> limitList = queryParams.get("limit");
|
||||
Integer limit = (limitList != null && limitList.size() == 1) ? Integer.parseInt(limitList.get(0)) : null;
|
||||
|
||||
List<String> offsetList = queryParams.get("offset");
|
||||
Integer offset = (offsetList != null && offsetList.size() == 1) ? Integer.parseInt(offsetList.get(0)) : null;
|
||||
|
||||
List<String> reverseList = queryParams.get("offset");
|
||||
Boolean reverse = (reverseList != null && reverseList.size() == 1) ? Boolean.getBoolean(reverseList.get(0)) : null;
|
||||
|
||||
List<String> txGroupIds = queryParams.get("txGroupId");
|
||||
if (txGroupIds != null && txGroupIds.size() == 1) {
|
||||
@@ -49,7 +58,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, null);
|
||||
null,
|
||||
encoding,
|
||||
limit, offset, reverse);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
} catch (DataException e) {
|
||||
@@ -79,7 +90,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
null,
|
||||
null,
|
||||
involvingAddresses,
|
||||
null, null, null);
|
||||
null,
|
||||
encoding,
|
||||
limit, offset, reverse);
|
||||
|
||||
sendMessages(session, chatMessages);
|
||||
} catch (DataException e) {
|
||||
@@ -105,7 +118,9 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
|
||||
@OnWebSocketMessage
|
||||
public void onWebSocketMessage(Session session, String message) {
|
||||
/* ignored */
|
||||
if (Objects.equals(message, "ping")) {
|
||||
session.getRemote().sendStringByFuture("pong");
|
||||
}
|
||||
}
|
||||
|
||||
private void onNotify(Session session, ChatTransactionData chatTransactionData, int txGroupId) {
|
||||
@@ -153,7 +168,7 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
// Convert ChatTransactionData to ChatMessage
|
||||
ChatMessage chatMessage;
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData);
|
||||
chatMessage = repository.getChatRepository().toChatMessage(chatTransactionData, getTargetEncoding(session));
|
||||
} catch (DataException e) {
|
||||
// No output this time?
|
||||
return;
|
||||
@@ -162,4 +177,12 @@ public class ChatMessagesWebSocket extends ApiWebSocket {
|
||||
sendMessages(session, Collections.singletonList(chatMessage));
|
||||
}
|
||||
|
||||
private Encoding getTargetEncoding(Session session) {
|
||||
// Default to Base58 if not specified, for backwards support
|
||||
Map<String, List<String>> queryParams = session.getUpgradeRequest().getParameterMap();
|
||||
List<String> encodingList = queryParams.get("encoding");
|
||||
String encoding = (encodingList != null && encodingList.size() == 1) ? encodingList.get(0) : "BASE58";
|
||||
return Encoding.valueOf(encoding);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
@@ -15,7 +16,6 @@ import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
@@ -85,6 +85,7 @@ public class ArbitraryDataFile {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chunks = new ArrayList<>();
|
||||
this.hash58 = Base58.encode(Crypto.digest(fileContent));
|
||||
this.signature = signature;
|
||||
LOGGER.trace(String.format("File digest: %s, size: %d bytes", this.hash58, fileContent.length));
|
||||
@@ -111,6 +112,41 @@ public class ArbitraryDataFile {
|
||||
return ArbitraryDataFile.fromHash58(Base58.encode(hash), signature);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile fromRawData(byte[] data, byte[] signature) throws DataException {
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
return new ArbitraryDataFile(data, signature);
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile fromTransactionData(ArbitraryTransactionData transactionData) throws DataException {
|
||||
ArbitraryDataFile arbitraryDataFile = null;
|
||||
byte[] signature = transactionData.getSignature();
|
||||
byte[] data = transactionData.getData();
|
||||
|
||||
if (data == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create data file
|
||||
switch (transactionData.getDataType()) {
|
||||
case DATA_HASH:
|
||||
arbitraryDataFile = ArbitraryDataFile.fromHash(data, signature);
|
||||
break;
|
||||
|
||||
case RAW_DATA:
|
||||
arbitraryDataFile = ArbitraryDataFile.fromRawData(data, signature);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set metadata hash
|
||||
if (arbitraryDataFile != null) {
|
||||
arbitraryDataFile.setMetadataHash(transactionData.getMetadataHash());
|
||||
}
|
||||
|
||||
return arbitraryDataFile;
|
||||
}
|
||||
|
||||
public static ArbitraryDataFile fromPath(Path path, byte[] signature) {
|
||||
if (path == null) {
|
||||
return null;
|
||||
@@ -260,6 +296,11 @@ public class ArbitraryDataFile {
|
||||
this.chunks = new ArrayList<>();
|
||||
|
||||
if (file != null) {
|
||||
if (file.exists() && file.length() <= chunkSize) {
|
||||
// No need to split into chunks if we're already below the chunk size
|
||||
return 0;
|
||||
}
|
||||
|
||||
try (FileInputStream fileInputStream = new FileInputStream(file);
|
||||
BufferedInputStream bis = new BufferedInputStream(fileInputStream)) {
|
||||
|
||||
@@ -388,12 +429,15 @@ public class ArbitraryDataFile {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean deleteAll() {
|
||||
public boolean deleteAll(boolean deleteMetadata) {
|
||||
// Delete the complete file
|
||||
boolean fileDeleted = this.delete();
|
||||
|
||||
// Delete the metadata file
|
||||
boolean metadataDeleted = this.deleteMetadata();
|
||||
// Delete the metadata file if requested
|
||||
boolean metadataDeleted = false;
|
||||
if (deleteMetadata) {
|
||||
metadataDeleted = this.deleteMetadata();
|
||||
}
|
||||
|
||||
// Delete the individual chunks
|
||||
boolean chunksDeleted = this.deleteAllChunks();
|
||||
@@ -612,6 +656,22 @@ public class ArbitraryDataFile {
|
||||
return this.chunks.size();
|
||||
}
|
||||
|
||||
public int fileCount() {
|
||||
int fileCount = this.chunkCount();
|
||||
|
||||
if (fileCount == 0) {
|
||||
// Transactions without any chunks can already be treated as a complete file
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
if (this.getMetadataHash() != null) {
|
||||
// Add the metadata file
|
||||
fileCount++;
|
||||
}
|
||||
|
||||
return fileCount;
|
||||
}
|
||||
|
||||
public List<ArbitraryDataFileChunk> getChunks() {
|
||||
return this.chunks;
|
||||
}
|
||||
|
@@ -19,10 +19,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.Transformer;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.ZipUtils;
|
||||
import org.qortal.utils.*;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
@@ -362,11 +359,6 @@ public class ArbitraryDataReader {
|
||||
throw new DataException(String.format("Transaction data not found for signature %s", this.resourceId));
|
||||
}
|
||||
|
||||
// Load hashes
|
||||
byte[] digest = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
byte[] signature = transactionData.getSignature();
|
||||
|
||||
// Load secret
|
||||
byte[] secret = transactionData.getSecret();
|
||||
if (secret != null) {
|
||||
@@ -374,16 +366,14 @@ public class ArbitraryDataReader {
|
||||
}
|
||||
|
||||
// Load data file(s)
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
ArbitraryTransactionUtils.checkAndRelocateMiscFiles(transactionData);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
|
||||
if (!arbitraryDataFile.allFilesExist()) {
|
||||
if (ArbitraryDataStorageManager.getInstance().isNameBlocked(transactionData.getName())) {
|
||||
if (ListUtils.isNameBlocked(transactionData.getName())) {
|
||||
throw new DataException(
|
||||
String.format("Unable to request missing data for file %s because the name is blocked", arbitraryDataFile));
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
// Ask the arbitrary data manager to fetch data for this transaction
|
||||
String message;
|
||||
if (this.canRequestMissingFiles) {
|
||||
@@ -394,8 +384,7 @@ public class ArbitraryDataReader {
|
||||
} else {
|
||||
message = String.format("Unable to reissue request for missing file %s for signature %s due to rate limit. Please try again later.", arbitraryDataFile, Base58.encode(transactionData.getSignature()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
message = String.format("Missing data for file %s", arbitraryDataFile);
|
||||
}
|
||||
|
||||
@@ -405,21 +394,25 @@ public class ArbitraryDataReader {
|
||||
}
|
||||
}
|
||||
|
||||
if (arbitraryDataFile.allChunksExist() && !arbitraryDataFile.exists()) {
|
||||
// We have all the chunks but not the complete file, so join them
|
||||
arbitraryDataFile.join();
|
||||
// Data hashes need some extra processing
|
||||
if (transactionData.getDataType() == DataType.DATA_HASH) {
|
||||
if (arbitraryDataFile.allChunksExist() && !arbitraryDataFile.exists()) {
|
||||
// We have all the chunks but not the complete file, so join them
|
||||
arbitraryDataFile.join();
|
||||
}
|
||||
|
||||
// If the complete file still doesn't exist then something went wrong
|
||||
if (!arbitraryDataFile.exists()) {
|
||||
throw new IOException(String.format("File doesn't exist: %s", arbitraryDataFile));
|
||||
}
|
||||
// Ensure the complete hash matches the joined chunks
|
||||
if (!Arrays.equals(arbitraryDataFile.digest(), transactionData.getData())) {
|
||||
// Delete the invalid file
|
||||
arbitraryDataFile.delete();
|
||||
throw new DataException("Unable to validate complete file hash");
|
||||
}
|
||||
}
|
||||
|
||||
// If the complete file still doesn't exist then something went wrong
|
||||
if (!arbitraryDataFile.exists()) {
|
||||
throw new IOException(String.format("File doesn't exist: %s", arbitraryDataFile));
|
||||
}
|
||||
// Ensure the complete hash matches the joined chunks
|
||||
if (!Arrays.equals(arbitraryDataFile.digest(), digest)) {
|
||||
// Delete the invalid file
|
||||
arbitraryDataFile.delete();
|
||||
throw new DataException("Unable to validate complete file hash");
|
||||
}
|
||||
// Ensure the file's size matches the size reported by the transaction (throws a DataException if not)
|
||||
arbitraryDataFile.validateFileSize(transactionData.getSize());
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.arbitrary;
|
||||
|
||||
import com.google.common.io.Resources;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.api.HTMLParser;
|
||||
@@ -34,28 +35,32 @@ public class ArbitraryDataRenderer {
|
||||
private final String resourceId;
|
||||
private final ResourceIdType resourceIdType;
|
||||
private final Service service;
|
||||
private final String identifier;
|
||||
private String theme = "light";
|
||||
private String inPath;
|
||||
private final String secret58;
|
||||
private final String prefix;
|
||||
private final boolean usePrefix;
|
||||
private final boolean async;
|
||||
private final String qdnContext;
|
||||
private final HttpServletRequest request;
|
||||
private final HttpServletResponse response;
|
||||
private final ServletContext context;
|
||||
|
||||
public ArbitraryDataRenderer(String resourceId, ResourceIdType resourceIdType, Service service, String inPath,
|
||||
String secret58, String prefix, boolean usePrefix, boolean async,
|
||||
public ArbitraryDataRenderer(String resourceId, ResourceIdType resourceIdType, Service service, String identifier,
|
||||
String inPath, String secret58, String prefix, boolean usePrefix, boolean async, String qdnContext,
|
||||
HttpServletRequest request, HttpServletResponse response, ServletContext context) {
|
||||
|
||||
this.resourceId = resourceId;
|
||||
this.resourceIdType = resourceIdType;
|
||||
this.service = service;
|
||||
this.identifier = identifier != null ? identifier : "default";
|
||||
this.inPath = inPath;
|
||||
this.secret58 = secret58;
|
||||
this.prefix = prefix;
|
||||
this.usePrefix = usePrefix;
|
||||
this.async = async;
|
||||
this.qdnContext = qdnContext;
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.context = context;
|
||||
@@ -71,14 +76,14 @@ public class ArbitraryDataRenderer {
|
||||
return ArbitraryDataRenderer.getResponse(response, 500, "QDN is disabled in settings");
|
||||
}
|
||||
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(resourceId, resourceIdType, service, null);
|
||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(resourceId, resourceIdType, service, identifier);
|
||||
arbitraryDataReader.setSecret58(secret58); // Optional, used for loading encrypted file hashes only
|
||||
try {
|
||||
if (!arbitraryDataReader.isCachedDataAvailable()) {
|
||||
// If async is requested, show a loading screen whilst build is in progress
|
||||
if (async) {
|
||||
arbitraryDataReader.loadAsynchronously(false, 10);
|
||||
return this.getLoadingResponse(service, resourceId, theme);
|
||||
return this.getLoadingResponse(service, resourceId, identifier, theme);
|
||||
}
|
||||
|
||||
// Otherwise, loop until we have data
|
||||
@@ -111,23 +116,57 @@ public class ArbitraryDataRenderer {
|
||||
}
|
||||
String unzippedPath = path.toString();
|
||||
|
||||
// Set path automatically for single file resources (except for apps, which handle routing differently)
|
||||
String[] files = ArrayUtils.removeElement(new File(unzippedPath).list(), ".qortal");
|
||||
if (files.length == 1 && this.service != Service.APP) {
|
||||
// This is a single file resource
|
||||
inPath = files[0];
|
||||
}
|
||||
|
||||
try {
|
||||
String filename = this.getFilename(unzippedPath, inPath);
|
||||
String filePath = Paths.get(unzippedPath, filename).toString();
|
||||
Path filePath = Paths.get(unzippedPath, filename);
|
||||
|
||||
// If the file doesn't exist, we may need to route the request elsewhere, or cleanup
|
||||
if (!Files.exists(filePath)) {
|
||||
if (inPath.equals("/")) {
|
||||
// Delete the unzipped folder if no index file was found
|
||||
try {
|
||||
FileUtils.deleteDirectory(new File(unzippedPath));
|
||||
} catch (IOException e) {
|
||||
LOGGER.debug("Unable to delete directory: {}", unzippedPath, e);
|
||||
}
|
||||
}
|
||||
|
||||
// If this is an app, then forward all unhandled requests to the index, to give the app the option to route it
|
||||
if (this.service == Service.APP) {
|
||||
// Locate index file
|
||||
List<String> indexFiles = ArbitraryDataRenderer.indexFiles();
|
||||
for (String indexFile : indexFiles) {
|
||||
Path indexPath = Paths.get(unzippedPath, indexFile);
|
||||
if (Files.exists(indexPath)) {
|
||||
// Forward request to index file
|
||||
filePath = indexPath;
|
||||
filename = indexFile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (HTMLParser.isHtmlFile(filename)) {
|
||||
// HTML file - needs to be parsed
|
||||
byte[] data = Files.readAllBytes(Paths.get(filePath)); // TODO: limit file size that can be read into memory
|
||||
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data);
|
||||
byte[] data = Files.readAllBytes(filePath); // TODO: limit file size that can be read into memory
|
||||
HTMLParser htmlParser = new HTMLParser(resourceId, inPath, prefix, usePrefix, data, qdnContext, service, identifier, theme);
|
||||
htmlParser.addAdditionalHeaderTags();
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' blob:; img-src 'self' data: blob:;");
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self' 'unsafe-inline' 'unsafe-eval'; media-src 'self' data: blob:; img-src 'self' data: blob:;");
|
||||
response.setContentType(context.getMimeType(filename));
|
||||
response.setContentLength(htmlParser.getData().length);
|
||||
response.getOutputStream().write(htmlParser.getData());
|
||||
}
|
||||
else {
|
||||
// Regular file - can be streamed directly
|
||||
File file = new File(filePath);
|
||||
File file = filePath.toFile();
|
||||
FileInputStream inputStream = new FileInputStream(file);
|
||||
response.addHeader("Content-Security-Policy", "default-src 'self'");
|
||||
response.setContentType(context.getMimeType(filename));
|
||||
@@ -143,14 +182,6 @@ public class ArbitraryDataRenderer {
|
||||
return response;
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
LOGGER.info("Unable to serve file: {}", e.getMessage());
|
||||
if (inPath.equals("/")) {
|
||||
// Delete the unzipped folder if no index file was found
|
||||
try {
|
||||
FileUtils.deleteDirectory(new File(unzippedPath));
|
||||
} catch (IOException ioException) {
|
||||
LOGGER.debug("Unable to delete directory: {}", unzippedPath, e);
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.info("Unable to serve file at path {}: {}", inPath, e.getMessage());
|
||||
}
|
||||
@@ -172,7 +203,7 @@ public class ArbitraryDataRenderer {
|
||||
return userPath;
|
||||
}
|
||||
|
||||
private HttpServletResponse getLoadingResponse(Service service, String name, String theme) {
|
||||
private HttpServletResponse getLoadingResponse(Service service, String name, String identifier, String theme) {
|
||||
String responseString = "";
|
||||
URL url = Resources.getResource("loading/index.html");
|
||||
try {
|
||||
@@ -181,6 +212,7 @@ public class ArbitraryDataRenderer {
|
||||
// Replace vars
|
||||
responseString = responseString.replace("%%SERVICE%%", service.toString());
|
||||
responseString = responseString.replace("%%NAME%%", name);
|
||||
responseString = responseString.replace("%%IDENTIFIER%%", identifier);
|
||||
responseString = responseString.replace("%%THEME%%", theme);
|
||||
|
||||
} catch (IOException e) {
|
||||
|
@@ -11,13 +11,13 @@ import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.ListUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -43,6 +43,7 @@ public class ArbitraryDataResource {
|
||||
private int layerCount;
|
||||
private Integer localChunkCount = null;
|
||||
private Integer totalChunkCount = null;
|
||||
private boolean exists = false;
|
||||
|
||||
public ArbitraryDataResource(String resourceId, ResourceIdType resourceIdType, Service service, String identifier) {
|
||||
this.resourceId = resourceId.toLowerCase();
|
||||
@@ -61,6 +62,10 @@ public class ArbitraryDataResource {
|
||||
// Avoid this for "quick" statuses, to speed things up
|
||||
if (!quick) {
|
||||
this.calculateChunkCounts();
|
||||
|
||||
if (!this.exists) {
|
||||
return new ArbitraryResourceStatus(Status.NOT_PUBLISHED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
}
|
||||
|
||||
if (resourceIdType != ResourceIdType.NAME) {
|
||||
@@ -69,8 +74,7 @@ public class ArbitraryDataResource {
|
||||
}
|
||||
|
||||
// Check if the name is blocked
|
||||
if (ResourceListManager.getInstance()
|
||||
.listContains("blockedNames", this.resourceId, false)) {
|
||||
if (ListUtils.isNameBlocked(this.resourceId)) {
|
||||
return new ArbitraryResourceStatus(Status.BLOCKED, this.localChunkCount, this.totalChunkCount);
|
||||
}
|
||||
|
||||
@@ -135,21 +139,20 @@ public class ArbitraryDataResource {
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean delete() {
|
||||
public boolean delete(boolean deleteMetadata) {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
if (this.transactions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList = new ArrayList<>(this.transactions);
|
||||
|
||||
for (ArbitraryTransactionData transactionData : transactionDataList) {
|
||||
byte[] hash = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
byte[] signature = transactionData.getSignature();
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
// Delete any chunks or complete files from each transaction
|
||||
arbitraryDataFile.deleteAll();
|
||||
arbitraryDataFile.deleteAll(deleteMetadata);
|
||||
}
|
||||
|
||||
// Also delete cached data for the entire resource
|
||||
@@ -193,6 +196,9 @@ public class ArbitraryDataResource {
|
||||
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
if (this.transactions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList = new ArrayList<>(this.transactions);
|
||||
|
||||
@@ -212,6 +218,14 @@ public class ArbitraryDataResource {
|
||||
private void calculateChunkCounts() {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
if (this.transactions == null) {
|
||||
this.exists = false;
|
||||
this.localChunkCount = 0;
|
||||
this.totalChunkCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
this.exists = true;
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList = new ArrayList<>(this.transactions);
|
||||
int localChunkCount = 0;
|
||||
@@ -231,6 +245,9 @@ public class ArbitraryDataResource {
|
||||
private boolean isRateLimited() {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
if (this.transactions == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<ArbitraryTransactionData> transactionDataList = new ArrayList<>(this.transactions);
|
||||
|
||||
@@ -254,6 +271,10 @@ public class ArbitraryDataResource {
|
||||
private boolean isDataPotentiallyAvailable() {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
if (this.transactions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return false;
|
||||
@@ -285,6 +306,10 @@ public class ArbitraryDataResource {
|
||||
private boolean isDownloading() {
|
||||
try {
|
||||
this.fetchTransactions();
|
||||
if (this.transactions == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Long now = NTP.getTime();
|
||||
if (now == null) {
|
||||
return false;
|
||||
@@ -337,7 +362,10 @@ public class ArbitraryDataResource {
|
||||
this.transactions = transactionDataList;
|
||||
this.layerCount = transactionDataList.size();
|
||||
|
||||
} catch (DataException e) {
|
||||
} catch (DataNotPublishedException e) {
|
||||
// Ignore without logging
|
||||
}
|
||||
catch (DataException e) {
|
||||
LOGGER.info(String.format("Repository error when fetching transactions for resource %s: %s", this, e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import org.qortal.arbitrary.metadata.ArbitraryDataMetadataPatch;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.crypto.AES;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
@@ -46,6 +47,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
private static final double MAX_FILE_DIFF = 0.5f;
|
||||
|
||||
private final String publicKey58;
|
||||
private final long fee;
|
||||
private final Path path;
|
||||
private final String name;
|
||||
private Method method;
|
||||
@@ -64,11 +66,12 @@ public class ArbitraryDataTransactionBuilder {
|
||||
private ArbitraryTransactionData arbitraryTransactionData;
|
||||
private ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, Path path, String name,
|
||||
public ArbitraryDataTransactionBuilder(Repository repository, String publicKey58, long fee, Path path, String name,
|
||||
Method method, Service service, String identifier,
|
||||
String title, String description, List<String> tags, Category category) {
|
||||
this.repository = repository;
|
||||
this.publicKey58 = publicKey58;
|
||||
this.fee = fee;
|
||||
this.path = path;
|
||||
this.name = name;
|
||||
this.method = method;
|
||||
@@ -179,6 +182,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
for (ModifiedPath path : metadata.getModifiedPaths()) {
|
||||
if (path.getDiffType() != DiffType.COMPLETE_FILE) {
|
||||
atLeastOnePatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +191,14 @@ public class ArbitraryDataTransactionBuilder {
|
||||
return Method.PUT;
|
||||
}
|
||||
|
||||
// We can't use PATCH for on-chain data because this requires the .qortal directory, which can't be put on chain
|
||||
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(this.path, false);
|
||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(FilesystemUtils.getSingleFileContents(path).length) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||
if (shouldUseOnChainData) {
|
||||
LOGGER.info("Data size is small enough to go on chain - using PUT");
|
||||
return Method.PUT;
|
||||
}
|
||||
|
||||
// State is appropriate for a PATCH transaction
|
||||
return Method.PATCH;
|
||||
}
|
||||
@@ -227,10 +239,12 @@ public class ArbitraryDataTransactionBuilder {
|
||||
random.nextBytes(lastReference);
|
||||
}
|
||||
|
||||
Compression compression = Compression.ZIP;
|
||||
// Single file resources are handled differently, especially for very small data payloads, as these go on chain
|
||||
final boolean isSingleFileResource = FilesystemUtils.isSingleFileResource(path, false);
|
||||
final boolean shouldUseOnChainData = (isSingleFileResource && AES.getEncryptedFileSize(FilesystemUtils.getSingleFileContents(path).length) <= ArbitraryTransaction.MAX_DATA_SIZE);
|
||||
|
||||
// FUTURE? Use zip compression for directories, or no compression for single files
|
||||
// Compression compression = (path.toFile().isDirectory()) ? Compression.ZIP : Compression.NONE;
|
||||
// Use zip compression if data isn't going on chain
|
||||
Compression compression = shouldUseOnChainData ? Compression.NONE : Compression.ZIP;
|
||||
|
||||
ArbitraryDataWriter arbitraryDataWriter = new ArbitraryDataWriter(path, name, service, identifier, method,
|
||||
compression, title, description, tags, category);
|
||||
@@ -248,45 +262,52 @@ public class ArbitraryDataTransactionBuilder {
|
||||
throw new DataException("Arbitrary data file is null");
|
||||
}
|
||||
|
||||
// Get chunks metadata file
|
||||
// Get metadata file
|
||||
ArbitraryDataFile metadataFile = arbitraryDataFile.getMetadataFile();
|
||||
if (metadataFile == null && arbitraryDataFile.chunkCount() > 1) {
|
||||
throw new DataException(String.format("Chunks metadata data file is null but there are %d chunks", arbitraryDataFile.chunkCount()));
|
||||
}
|
||||
|
||||
String digest58 = arbitraryDataFile.digest58();
|
||||
if (digest58 == null) {
|
||||
LOGGER.error("Unable to calculate file digest");
|
||||
throw new DataException("Unable to calculate file digest");
|
||||
// Default to using a data hash, with data held off-chain
|
||||
ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH;
|
||||
byte[] data = arbitraryDataFile.digest();
|
||||
|
||||
// For small, single-chunk resources, we can store the data directly on chain
|
||||
if (shouldUseOnChainData && arbitraryDataFile.getBytes().length <= ArbitraryTransaction.MAX_DATA_SIZE && arbitraryDataFile.chunkCount() == 0) {
|
||||
// Within allowed on-chain data size
|
||||
dataType = DataType.RAW_DATA;
|
||||
data = arbitraryDataFile.getBytes();
|
||||
}
|
||||
|
||||
final BaseTransactionData baseTransactionData = new BaseTransactionData(now, Group.NO_GROUP,
|
||||
lastReference, creatorPublicKey, 0L, null);
|
||||
lastReference, creatorPublicKey, fee, null);
|
||||
final int size = (int) arbitraryDataFile.size();
|
||||
final int version = 5;
|
||||
final int nonce = 0;
|
||||
byte[] secret = arbitraryDataFile.getSecret();
|
||||
final ArbitraryTransactionData.DataType dataType = ArbitraryTransactionData.DataType.DATA_HASH;
|
||||
final byte[] digest = arbitraryDataFile.digest();
|
||||
|
||||
final byte[] metadataHash = (metadataFile != null) ? metadataFile.getHash() : null;
|
||||
final List<PaymentData> payments = new ArrayList<>();
|
||||
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, service, nonce, size, name, identifier, method,
|
||||
secret, compression, digest, dataType, metadataHash, payments);
|
||||
version, service.value, nonce, size, name, identifier, method,
|
||||
secret, compression, data, dataType, metadataHash, payments);
|
||||
|
||||
this.arbitraryTransactionData = transactionData;
|
||||
|
||||
} catch (DataException e) {
|
||||
} catch (DataException | IOException e) {
|
||||
if (arbitraryDataFile != null) {
|
||||
arbitraryDataFile.deleteAll();
|
||||
arbitraryDataFile.deleteAll(true);
|
||||
}
|
||||
throw(e);
|
||||
throw new DataException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private boolean isMetadataEqual(ArbitraryDataTransactionMetadata existingMetadata) {
|
||||
if (existingMetadata == null) {
|
||||
return !this.hasMetadata();
|
||||
}
|
||||
if (!Objects.equals(existingMetadata.getTitle(), this.title)) {
|
||||
return false;
|
||||
}
|
||||
@@ -302,6 +323,10 @@ public class ArbitraryDataTransactionBuilder {
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean hasMetadata() {
|
||||
return (this.title != null || this.description != null || this.category != null || this.tags != null);
|
||||
}
|
||||
|
||||
public void computeNonce() throws DataException {
|
||||
if (this.arbitraryTransactionData == null) {
|
||||
throw new DataException("Arbitrary transaction data is required to compute nonce");
|
||||
@@ -313,7 +338,7 @@ public class ArbitraryDataTransactionBuilder {
|
||||
|
||||
Transaction.ValidationResult result = transaction.isValidUnconfirmed();
|
||||
if (result != Transaction.ValidationResult.OK) {
|
||||
arbitraryDataFile.deleteAll();
|
||||
arbitraryDataFile.deleteAll(true);
|
||||
throw new DataException(String.format("Arbitrary transaction invalid: %s", result));
|
||||
}
|
||||
LOGGER.info("Transaction is valid");
|
||||
|
@@ -1,5 +1,7 @@
|
||||
package org.qortal.arbitrary;
|
||||
|
||||
import com.j256.simplemagic.ContentInfo;
|
||||
import com.j256.simplemagic.ContentInfoUtil;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
@@ -23,6 +25,8 @@ import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.FileNameMap;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.file.*;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
@@ -48,6 +52,7 @@ public class ArbitraryDataWriter {
|
||||
private final List<String> tags;
|
||||
private final Category category;
|
||||
private List<String> files;
|
||||
private String mimeType;
|
||||
|
||||
private int chunkSize = ArbitraryDataFile.CHUNK_SIZE;
|
||||
|
||||
@@ -79,6 +84,7 @@ public class ArbitraryDataWriter {
|
||||
this.tags = ArbitraryDataTransactionMetadata.limitTags(tags);
|
||||
this.category = category;
|
||||
this.files = new ArrayList<>(); // Populated in buildFileList()
|
||||
this.mimeType = null; // Populated in buildFileList()
|
||||
}
|
||||
|
||||
public void save() throws IOException, DataException, InterruptedException, MissingDataException {
|
||||
@@ -101,10 +107,9 @@ public class ArbitraryDataWriter {
|
||||
private void preExecute() throws DataException {
|
||||
this.checkEnabled();
|
||||
|
||||
// Enforce compression when uploading a directory
|
||||
File file = new File(this.filePath.toString());
|
||||
if (file.isDirectory() && compression == Compression.NONE) {
|
||||
throw new DataException("Unable to upload a directory without compression");
|
||||
// Enforce compression when uploading multiple files
|
||||
if (!FilesystemUtils.isSingleFileResource(this.filePath, false) && compression == Compression.NONE) {
|
||||
throw new DataException("Unable to publish multiple files without compression");
|
||||
}
|
||||
|
||||
// Create temporary working directory
|
||||
@@ -144,20 +149,44 @@ public class ArbitraryDataWriter {
|
||||
}
|
||||
|
||||
private void buildFileList() throws IOException {
|
||||
// Single file resources consist of a single element in the file list
|
||||
// Check if the path already points to a single file
|
||||
boolean isSingleFile = this.filePath.toFile().isFile();
|
||||
Path singleFilePath = null;
|
||||
if (isSingleFile) {
|
||||
this.files.add(this.filePath.getFileName().toString());
|
||||
return;
|
||||
singleFilePath = this.filePath;
|
||||
}
|
||||
else {
|
||||
// Multi file resources (or a single file in a directory) require a walk through the directory tree
|
||||
try (Stream<Path> stream = Files.walk(this.filePath)) {
|
||||
this.files = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.map(p -> this.filePath.relativize(p).toString())
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (this.files.size() == 1) {
|
||||
singleFilePath = Paths.get(this.filePath.toString(), this.files.get(0));
|
||||
|
||||
// Update filePath to point to the single file (instead of the directory containing the file)
|
||||
this.filePath = singleFilePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Multi file resources require a walk through the directory tree
|
||||
try (Stream<Path> stream = Files.walk(this.filePath)) {
|
||||
this.files = stream
|
||||
.filter(Files::isRegularFile)
|
||||
.map(p -> this.filePath.relativize(p).toString())
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(Collectors.toList());
|
||||
if (singleFilePath != null) {
|
||||
// Single file resource, so try and determine the MIME type
|
||||
ContentInfoUtil util = new ContentInfoUtil();
|
||||
ContentInfo info = util.findMatch(singleFilePath.toFile());
|
||||
if (info != null) {
|
||||
// Attempt to extract MIME type from file contents
|
||||
this.mimeType = info.getMimeType();
|
||||
}
|
||||
else {
|
||||
// Fall back to using the filename
|
||||
FileNameMap fileNameMap = URLConnection.getFileNameMap();
|
||||
this.mimeType = fileNameMap.getContentTypeFor(singleFilePath.toFile().getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,9 +316,6 @@ public class ArbitraryDataWriter {
|
||||
if (chunkCount > 0) {
|
||||
LOGGER.info(String.format("Successfully split into %d chunk%s", chunkCount, (chunkCount == 1 ? "" : "s")));
|
||||
}
|
||||
else {
|
||||
throw new DataException("Unable to split file into chunks");
|
||||
}
|
||||
}
|
||||
|
||||
private void createMetadataFile() throws IOException, DataException {
|
||||
@@ -304,6 +330,7 @@ public class ArbitraryDataWriter {
|
||||
metadata.setCategory(this.category);
|
||||
metadata.setChunks(this.arbitraryDataFile.chunkHashList());
|
||||
metadata.setFiles(this.files);
|
||||
metadata.setMimeType(this.mimeType);
|
||||
metadata.write();
|
||||
|
||||
// Create an ArbitraryDataFile from the JSON file (we don't have a signature yet)
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.arbitrary.metadata;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONException;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
@@ -34,7 +35,7 @@ public class ArbitraryDataMetadata {
|
||||
this.filePath = filePath;
|
||||
}
|
||||
|
||||
protected void readJson() throws DataException {
|
||||
protected void readJson() throws DataException, JSONException {
|
||||
// To be overridden
|
||||
}
|
||||
|
||||
@@ -44,8 +45,13 @@ public class ArbitraryDataMetadata {
|
||||
|
||||
|
||||
public void read() throws IOException, DataException {
|
||||
this.loadJson();
|
||||
this.readJson();
|
||||
try {
|
||||
this.loadJson();
|
||||
this.readJson();
|
||||
|
||||
} catch (JSONException e) {
|
||||
throw new DataException(String.format("Unable to read JSON: %s", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
public void write() throws IOException, DataException {
|
||||
|
@@ -1,5 +1,6 @@
|
||||
package org.qortal.arbitrary.metadata;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.utils.Base58;
|
||||
@@ -22,7 +23,7 @@ public class ArbitraryDataMetadataCache extends ArbitraryDataQortalMetadata {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readJson() throws DataException {
|
||||
protected void readJson() throws DataException, JSONException {
|
||||
if (this.jsonString == null) {
|
||||
throw new DataException("Patch JSON string is null");
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ package org.qortal.arbitrary.metadata;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.arbitrary.ArbitraryDataDiff.*;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -40,7 +41,7 @@ public class ArbitraryDataMetadataPatch extends ArbitraryDataQortalMetadata {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readJson() throws DataException {
|
||||
protected void readJson() throws DataException, JSONException {
|
||||
if (this.jsonString == null) {
|
||||
throw new DataException("Patch JSON string is null");
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package org.qortal.arbitrary.metadata;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.json.JSONException;
|
||||
import org.qortal.repository.DataException;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
@@ -46,20 +47,6 @@ public class ArbitraryDataQortalMetadata extends ArbitraryDataMetadata {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void readJson() throws DataException {
|
||||
// To be overridden
|
||||
}
|
||||
|
||||
protected void buildJson() {
|
||||
// To be overridden
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void read() throws IOException, DataException {
|
||||
this.loadJson();
|
||||
this.readJson();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write() throws IOException, DataException {
|
||||
@@ -94,9 +81,4 @@ public class ArbitraryDataQortalMetadata extends ArbitraryDataMetadata {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getJsonString() {
|
||||
return this.jsonString;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package org.qortal.arbitrary.metadata;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.qortal.arbitrary.misc.Category;
|
||||
import org.qortal.repository.DataException;
|
||||
@@ -20,9 +21,10 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
private List<String> tags;
|
||||
private Category category;
|
||||
private List<String> files;
|
||||
private String mimeType;
|
||||
|
||||
private static int MAX_TITLE_LENGTH = 80;
|
||||
private static int MAX_DESCRIPTION_LENGTH = 500;
|
||||
private static int MAX_DESCRIPTION_LENGTH = 240;
|
||||
private static int MAX_TAG_LENGTH = 20;
|
||||
private static int MAX_TAGS_COUNT = 5;
|
||||
|
||||
@@ -32,7 +34,7 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void readJson() throws DataException {
|
||||
protected void readJson() throws DataException, JSONException {
|
||||
if (this.jsonString == null) {
|
||||
throw new DataException("Transaction metadata JSON string is null");
|
||||
}
|
||||
@@ -92,6 +94,10 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
}
|
||||
this.files = filesList;
|
||||
}
|
||||
|
||||
if (metadata.has("mimeType")) {
|
||||
this.mimeType = metadata.getString("mimeType");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -134,6 +140,10 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
}
|
||||
outer.put("files", files);
|
||||
|
||||
if (this.mimeType != null && !this.mimeType.isEmpty()) {
|
||||
outer.put("mimeType", this.mimeType);
|
||||
}
|
||||
|
||||
this.jsonString = outer.toString(2);
|
||||
LOGGER.trace("Transaction metadata: {}", this.jsonString);
|
||||
}
|
||||
@@ -187,6 +197,14 @@ public class ArbitraryDataTransactionMetadata extends ArbitraryDataMetadata {
|
||||
return this.files;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return this.mimeType;
|
||||
}
|
||||
|
||||
public boolean containsChunk(byte[] chunk) {
|
||||
for (byte[] c : this.chunks) {
|
||||
if (Arrays.equals(c, chunk)) {
|
||||
|
@@ -8,17 +8,21 @@ import org.qortal.utils.FilesystemUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.*;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import static java.util.Arrays.stream;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
public enum Service {
|
||||
AUTO_UPDATE(1, false, null, null),
|
||||
ARBITRARY_DATA(100, false, null, null),
|
||||
QCHAT_ATTACHMENT(120, true, 1024*1024L, null) {
|
||||
AUTO_UPDATE(1, false, null, false, null),
|
||||
ARBITRARY_DATA(100, false, null, false, null),
|
||||
QCHAT_ATTACHMENT(120, true, 1024*1024L, true, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
@@ -26,37 +30,24 @@ public enum Service {
|
||||
return superclassResult;
|
||||
}
|
||||
|
||||
// Custom validation function to require a single file, with a whitelisted extension
|
||||
int fileCount = 0;
|
||||
File[] files = path.toFile().listFiles();
|
||||
// If already a single file, replace the list with one that contains that file only
|
||||
if (files == null && path.toFile().isFile()) {
|
||||
files = new File[] { path.toFile() };
|
||||
}
|
||||
if (files != null) {
|
||||
for (File file : files) {
|
||||
if (file.getName().equals(".qortal")) {
|
||||
continue;
|
||||
}
|
||||
if (file.isDirectory()) {
|
||||
return ValidationResult.DIRECTORIES_NOT_ALLOWED;
|
||||
}
|
||||
final String extension = FilenameUtils.getExtension(file.getName()).toLowerCase();
|
||||
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
|
||||
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||
if (extension == null || !allowedExtensions.contains(extension)) {
|
||||
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||
}
|
||||
fileCount++;
|
||||
// Now validate the file's extension
|
||||
if (files != null && files[0] != null) {
|
||||
final String extension = FilenameUtils.getExtension(files[0].getName()).toLowerCase();
|
||||
// We must allow blank file extensions because these are used by data published from a plaintext or base64-encoded string
|
||||
final List<String> allowedExtensions = Arrays.asList("zip", "pdf", "txt", "odt", "ods", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "");
|
||||
if (extension == null || !allowedExtensions.contains(extension)) {
|
||||
return ValidationResult.INVALID_FILE_EXTENSION;
|
||||
}
|
||||
}
|
||||
if (fileCount != 1) {
|
||||
return ValidationResult.INVALID_FILE_COUNT;
|
||||
}
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
},
|
||||
WEBSITE(200, true, null, null) {
|
||||
WEBSITE(200, true, null, false, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
@@ -78,21 +69,42 @@ public enum Service {
|
||||
return ValidationResult.MISSING_INDEX_FILE;
|
||||
}
|
||||
},
|
||||
GIT_REPOSITORY(300, false, null, null),
|
||||
IMAGE(400, true, 10*1024*1024L, null),
|
||||
THUMBNAIL(410, true, 500*1024L, null),
|
||||
QCHAT_IMAGE(420, true, 500*1024L, null),
|
||||
VIDEO(500, false, null, null),
|
||||
AUDIO(600, false, null, null),
|
||||
BLOG(700, false, null, null),
|
||||
BLOG_POST(777, false, null, null),
|
||||
BLOG_COMMENT(778, false, null, null),
|
||||
DOCUMENT(800, false, null, null),
|
||||
LIST(900, true, null, null),
|
||||
PLAYLIST(910, true, null, null),
|
||||
APP(1000, false, null, null),
|
||||
METADATA(1100, false, null, null),
|
||||
GIF_REPOSITORY(1200, true, 25*1024*1024L, null) {
|
||||
GIT_REPOSITORY(300, false, null, false, null),
|
||||
IMAGE(400, true, 10*1024*1024L, true, null),
|
||||
THUMBNAIL(410, true, 500*1024L, true, null),
|
||||
QCHAT_IMAGE(420, true, 500*1024L, true, null),
|
||||
VIDEO(500, false, null, true, null),
|
||||
AUDIO(600, false, null, true, null),
|
||||
QCHAT_AUDIO(610, true, 10*1024*1024L, true, null),
|
||||
QCHAT_VOICE(620, true, 10*1024*1024L, true, null),
|
||||
BLOG(700, false, null, false, null),
|
||||
BLOG_POST(777, false, null, true, null),
|
||||
BLOG_COMMENT(778, false, null, true, null),
|
||||
DOCUMENT(800, false, null, true, null),
|
||||
LIST(900, true, null, true, null),
|
||||
PLAYLIST(910, true, null, true, null),
|
||||
APP(1000, true, 50*1024*1024L, false, null),
|
||||
METADATA(1100, false, null, true, null),
|
||||
JSON(1110, true, 25*1024L, true, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
if (superclassResult != ValidationResult.OK) {
|
||||
return superclassResult;
|
||||
}
|
||||
|
||||
// Require valid JSON
|
||||
byte[] data = FilesystemUtils.getSingleFileContents(path);
|
||||
String json = new String(data, StandardCharsets.UTF_8);
|
||||
try {
|
||||
objectMapper.readTree(json);
|
||||
return ValidationResult.OK;
|
||||
} catch (IOException e) {
|
||||
return ValidationResult.INVALID_CONTENT;
|
||||
}
|
||||
}
|
||||
},
|
||||
GIF_REPOSITORY(1200, true, 25*1024*1024L, false, null) {
|
||||
@Override
|
||||
public ValidationResult validate(Path path) throws IOException {
|
||||
ValidationResult superclassResult = super.validate(path);
|
||||
@@ -132,15 +144,20 @@ public enum Service {
|
||||
public final int value;
|
||||
private final boolean requiresValidation;
|
||||
private final Long maxSize;
|
||||
private final boolean single;
|
||||
private final List<String> requiredKeys;
|
||||
|
||||
private static final Map<Integer, Service> map = stream(Service.values())
|
||||
.collect(toMap(service -> service.value, service -> service));
|
||||
|
||||
Service(int value, boolean requiresValidation, Long maxSize, List<String> requiredKeys) {
|
||||
// For JSON validation
|
||||
private static final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
Service(int value, boolean requiresValidation, Long maxSize, boolean single, List<String> requiredKeys) {
|
||||
this.value = value;
|
||||
this.requiresValidation = requiresValidation;
|
||||
this.maxSize = maxSize;
|
||||
this.single = single;
|
||||
this.requiredKeys = requiredKeys;
|
||||
}
|
||||
|
||||
@@ -159,6 +176,11 @@ public enum Service {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate file count if needed
|
||||
if (this.single && data == null) {
|
||||
return ValidationResult.INVALID_FILE_COUNT;
|
||||
}
|
||||
|
||||
// Validate required keys if needed
|
||||
if (this.requiredKeys != null) {
|
||||
if (data == null) {
|
||||
@@ -197,7 +219,8 @@ public enum Service {
|
||||
DIRECTORIES_NOT_ALLOWED(5),
|
||||
INVALID_FILE_EXTENSION(6),
|
||||
MISSING_DATA(7),
|
||||
INVALID_FILE_COUNT(8);
|
||||
INVALID_FILE_COUNT(8),
|
||||
INVALID_CONTENT(9);
|
||||
|
||||
public final int value;
|
||||
|
||||
|
@@ -657,6 +657,10 @@ public class Block {
|
||||
return this.atStates;
|
||||
}
|
||||
|
||||
public byte[] getAtStatesHash() {
|
||||
return this.atStatesHash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return expanded info on block's online accounts.
|
||||
* <p>
|
||||
|
@@ -78,7 +78,8 @@ public class BlockChain {
|
||||
onlineAccountMinterLevelValidationHeight,
|
||||
selfSponsorshipAlgoV1Height,
|
||||
feeValidationFixTimestamp,
|
||||
chatReferenceTimestamp;
|
||||
chatReferenceTimestamp,
|
||||
arbitraryOptionalFeeTimestamp;
|
||||
}
|
||||
|
||||
// Custom transaction fees
|
||||
@@ -522,6 +523,10 @@ public class BlockChain {
|
||||
return this.featureTriggers.get(FeatureTrigger.chatReferenceTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
public long getArbitraryOptionalFeeTimestamp() {
|
||||
return this.featureTriggers.get(FeatureTrigger.arbitraryOptionalFeeTimestamp.name()).longValue();
|
||||
}
|
||||
|
||||
|
||||
// More complex getters for aspects that change by height or timestamp
|
||||
|
||||
|
@@ -293,4 +293,77 @@ public class AutoUpdate extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean attemptRestart() {
|
||||
LOGGER.info(String.format("Restarting node..."));
|
||||
|
||||
// Give repository a chance to backup in case things go badly wrong (if enabled)
|
||||
if (Settings.getInstance().getRepositoryBackupInterval() > 0) {
|
||||
try {
|
||||
// Timeout if the database isn't ready for backing up after 60 seconds
|
||||
long timeout = 60 * 1000L;
|
||||
RepositoryManager.backup(true, "backup", timeout);
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
LOGGER.info("Attempt to backup repository failed due to timeout: {}", e.getMessage());
|
||||
// Continue with the node restart anyway...
|
||||
}
|
||||
}
|
||||
|
||||
// Call ApplyUpdate to end this process (unlocking current JAR so it can be replaced)
|
||||
String javaHome = System.getProperty("java.home");
|
||||
LOGGER.debug(String.format("Java home: %s", javaHome));
|
||||
|
||||
Path javaBinary = Paths.get(javaHome, "bin", "java");
|
||||
LOGGER.debug(String.format("Java binary: %s", javaBinary));
|
||||
|
||||
try {
|
||||
List<String> javaCmd = new ArrayList<>();
|
||||
// Java runtime binary itself
|
||||
javaCmd.add(javaBinary.toString());
|
||||
|
||||
// JVM arguments
|
||||
javaCmd.addAll(ManagementFactory.getRuntimeMXBean().getInputArguments());
|
||||
|
||||
// Disable, but retain, any -agentlib JVM arg as sub-process might fail if it tries to reuse same port
|
||||
javaCmd = javaCmd.stream()
|
||||
.map(arg -> arg.replace("-agentlib", AGENTLIB_JVM_HOLDER_ARG))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// Remove JNI options as they won't be supported by command-line 'java'
|
||||
// These are typically added by the AdvancedInstaller Java launcher EXE
|
||||
javaCmd.removeAll(Arrays.asList("abort", "exit", "vfprintf"));
|
||||
|
||||
// Call ApplyUpdate using JAR
|
||||
javaCmd.addAll(Arrays.asList("-cp", JAR_FILENAME, ApplyUpdate.class.getCanonicalName()));
|
||||
|
||||
// Add command-line args saved from start-up
|
||||
String[] savedArgs = Controller.getInstance().getSavedArgs();
|
||||
if (savedArgs != null)
|
||||
javaCmd.addAll(Arrays.asList(savedArgs));
|
||||
|
||||
LOGGER.info(String.format("Restarting node with: %s", String.join(" ", javaCmd)));
|
||||
|
||||
SysTray.getInstance().showMessage(Translator.INSTANCE.translate("SysTray", "AUTO_UPDATE"), //TODO
|
||||
Translator.INSTANCE.translate("SysTray", "APPLYING_UPDATE_AND_RESTARTING"), //TODO
|
||||
MessageType.INFO);
|
||||
|
||||
ProcessBuilder processBuilder = new ProcessBuilder(javaCmd);
|
||||
|
||||
// New process will inherit our stdout and stderr
|
||||
processBuilder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
|
||||
processBuilder.redirectError(ProcessBuilder.Redirect.INHERIT);
|
||||
|
||||
Process process = processBuilder.start();
|
||||
|
||||
// Nothing to pipe to new process, so close output stream (process's stdin)
|
||||
process.getOutputStream().close();
|
||||
|
||||
return true; // restarting node OK
|
||||
} catch (Exception e) {
|
||||
LOGGER.error(String.format("Failed to restart node: %s", e.getMessage()));
|
||||
|
||||
return true; // repo was okay, even if applying update failed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -432,6 +432,10 @@ public class BlockMinter extends Thread {
|
||||
// Unable to process block - report and discard
|
||||
LOGGER.error("Unable to process newly minted block?", e);
|
||||
newBlocks.clear();
|
||||
} catch (ArithmeticException e) {
|
||||
// Unable to process block - report and discard
|
||||
LOGGER.error("Unable to process newly minted block?", e);
|
||||
newBlocks.clear();
|
||||
}
|
||||
} finally {
|
||||
blockchainLock.unlock();
|
||||
|
@@ -400,12 +400,8 @@ public class Controller extends Thread {
|
||||
RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(getRepositoryUrl());
|
||||
RepositoryManager.setRepositoryFactory(repositoryFactory);
|
||||
RepositoryManager.setRequestedCheckpoint(Boolean.TRUE);
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
RepositoryManager.archive(repository);
|
||||
RepositoryManager.prune(repository);
|
||||
}
|
||||
} catch (DataException e) {
|
||||
}
|
||||
catch (DataException e) {
|
||||
// If exception has no cause then repository is in use by some other process.
|
||||
if (e.getCause() == null) {
|
||||
LOGGER.info("Repository in use by another process?");
|
||||
@@ -1379,9 +1375,24 @@ public class Controller extends Thread {
|
||||
// If we have no block data, we should check the archive in case it's there
|
||||
if (blockData == null) {
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
byte[] bytes = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, true, repository);
|
||||
if (bytes != null) {
|
||||
CachedBlockMessage blockMessage = new CachedBlockMessage(bytes);
|
||||
Triple<byte[], Integer, Integer> serializedBlock = BlockArchiveReader.getInstance().fetchSerializedBlockBytesForSignature(signature, true, repository);
|
||||
if (serializedBlock != null) {
|
||||
byte[] bytes = serializedBlock.getA();
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
|
||||
Message blockMessage;
|
||||
switch (serializationVersion) {
|
||||
case 1:
|
||||
blockMessage = new CachedBlockMessage(bytes);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
blockMessage = new CachedBlockV2Message(bytes);
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
blockMessage.setId(message.getId());
|
||||
|
||||
// This call also causes the other needed data to be pulled in from repository
|
||||
|
@@ -163,7 +163,7 @@ public class PirateChainWalletController extends Thread {
|
||||
|
||||
// Library not found, so check if we've fetched the resource from QDN
|
||||
ArbitraryTransactionData t = this.getTransactionData(repository);
|
||||
if (t == null) {
|
||||
if (t == null || t.getService() == null) {
|
||||
// Can't find the transaction - maybe on a different chain?
|
||||
return;
|
||||
}
|
||||
|
@@ -11,10 +11,7 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
@@ -137,7 +134,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Fetch the transaction data
|
||||
ArbitraryTransactionData arbitraryTransactionData = ArbitraryTransactionUtils.fetchTransactionData(repository, signature);
|
||||
if (arbitraryTransactionData == null) {
|
||||
if (arbitraryTransactionData == null || arbitraryTransactionData.getService() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -204,7 +201,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
if (completeFileExists && !allChunksExist) {
|
||||
// We have the complete file but not the chunks, so let's convert it
|
||||
LOGGER.info(String.format("Transaction %s has complete file but no chunks",
|
||||
LOGGER.debug(String.format("Transaction %s has complete file but no chunks",
|
||||
Base58.encode(arbitraryTransactionData.getSignature())));
|
||||
|
||||
ArbitraryTransactionUtils.convertFileToChunks(arbitraryTransactionData, now, STALE_FILE_TIMEOUT);
|
||||
@@ -239,7 +236,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Delete random data associated with name if we're over our storage limit for this name
|
||||
// Use the DELETION_THRESHOLD, for the same reasons as above
|
||||
for (String followedName : storageManager.followedNames()) {
|
||||
for (String followedName : ListUtils.followedNames()) {
|
||||
if (isStopping) {
|
||||
return;
|
||||
}
|
||||
@@ -487,7 +484,7 @@ public class ArbitraryDataCleanupManager extends Thread {
|
||||
|
||||
// Delete data relating to blocked names
|
||||
String name = directory.getName();
|
||||
if (name != null && storageManager.isNameBlocked(name)) {
|
||||
if (name != null && ListUtils.isNameBlocked(name)) {
|
||||
this.safeDeleteDirectory(directory, "blocked name");
|
||||
}
|
||||
|
||||
|
@@ -20,6 +20,7 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ListUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@@ -258,8 +259,6 @@ public class ArbitraryDataFileListManager {
|
||||
// Lookup file lists by signature (and optionally hashes)
|
||||
|
||||
public boolean fetchArbitraryDataFileList(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
byte[] digest = arbitraryTransactionData.getData();
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
String signature58 = Base58.encode(signature);
|
||||
|
||||
@@ -286,8 +285,7 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
// Find hashes that we are missing
|
||||
try {
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
missingHashes = arbitraryDataFile.missingHashes();
|
||||
} catch (DataException e) {
|
||||
// Leave missingHashes as null, so that all hashes are requested
|
||||
@@ -460,10 +458,9 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
|
||||
// Load data file(s)
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(arbitraryTransactionData.getData(), signature);
|
||||
arbitraryDataFile.setMetadataHash(arbitraryTransactionData.getMetadataHash());
|
||||
|
||||
// // Load data file(s)
|
||||
// ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
//
|
||||
// // Check all hashes exist
|
||||
// for (byte[] hash : hashes) {
|
||||
// //LOGGER.debug("Received hash {}", Base58.encode(hash));
|
||||
@@ -507,7 +504,7 @@ public class ArbitraryDataFileListManager {
|
||||
|
||||
// Forwarding
|
||||
if (isRelayRequest && Settings.getInstance().isRelayModeEnabled()) {
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(arbitraryTransactionData.getName()));
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||
if (!isBlocked) {
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
@@ -594,12 +591,8 @@ public class ArbitraryDataFileListManager {
|
||||
// Check if we're even allowed to serve data for this transaction
|
||||
if (ArbitraryDataStorageManager.getInstance().canStoreData(transactionData)) {
|
||||
|
||||
byte[] hash = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
|
||||
// Load file(s) and add any that exist to the list of hashes
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
// If the peer didn't supply a hash list, we need to return all hashes for this transaction
|
||||
if (requestedHashes == null || requestedHashes.isEmpty()) {
|
||||
@@ -690,7 +683,7 @@ public class ArbitraryDataFileListManager {
|
||||
}
|
||||
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(transactionData.getName()));
|
||||
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
|
@@ -132,9 +132,7 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
List<byte[]> hashes) throws DataException {
|
||||
|
||||
// Load data file(s)
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(arbitraryTransactionData.getData(), signature);
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
boolean receivedAtLeastOneFile = false;
|
||||
|
||||
// Now fetch actual data from this peer
|
||||
@@ -148,10 +146,10 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
if (!arbitraryDataFileRequests.containsKey(Base58.encode(hash))) {
|
||||
LOGGER.debug("Requesting data file {} from peer {}", hash58, peer);
|
||||
Long startTime = NTP.getTime();
|
||||
ArbitraryDataFileMessage receivedArbitraryDataFileMessage = fetchArbitraryDataFile(peer, null, signature, hash, null);
|
||||
ArbitraryDataFile receivedArbitraryDataFile = fetchArbitraryDataFile(peer, null, signature, hash, null);
|
||||
Long endTime = NTP.getTime();
|
||||
if (receivedArbitraryDataFileMessage != null && receivedArbitraryDataFileMessage.getArbitraryDataFile() != null) {
|
||||
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFileMessage.getArbitraryDataFile().getHash58(), peer, (endTime-startTime));
|
||||
if (receivedArbitraryDataFile != null) {
|
||||
LOGGER.debug("Received data file {} from peer {}. Time taken: {} ms", receivedArbitraryDataFile.getHash58(), peer, (endTime-startTime));
|
||||
receivedAtLeastOneFile = true;
|
||||
|
||||
// Remove this hash from arbitraryDataFileHashResponses now that we have received it
|
||||
@@ -193,11 +191,11 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
return receivedAtLeastOneFile;
|
||||
}
|
||||
|
||||
private ArbitraryDataFileMessage fetchArbitraryDataFile(Peer peer, Peer requestingPeer, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||
private ArbitraryDataFile fetchArbitraryDataFile(Peer peer, Peer requestingPeer, byte[] signature, byte[] hash, Message originalMessage) throws DataException {
|
||||
ArbitraryDataFile existingFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
boolean fileAlreadyExists = existingFile.exists();
|
||||
String hash58 = Base58.encode(hash);
|
||||
ArbitraryDataFileMessage arbitraryDataFileMessage;
|
||||
ArbitraryDataFile arbitraryDataFile;
|
||||
|
||||
// Fetch the file if it doesn't exist locally
|
||||
if (!fileAlreadyExists) {
|
||||
@@ -227,28 +225,32 @@ public class ArbitraryDataFileManager extends Thread {
|
||||
}
|
||||
|
||||
ArbitraryDataFileMessage peersArbitraryDataFileMessage = (ArbitraryDataFileMessage) response;
|
||||
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, peersArbitraryDataFileMessage.getArbitraryDataFile());
|
||||
arbitraryDataFile = peersArbitraryDataFileMessage.getArbitraryDataFile();
|
||||
} else {
|
||||
LOGGER.debug(String.format("File hash %s already exists, so skipping the request", hash58));
|
||||
arbitraryDataFileMessage = new ArbitraryDataFileMessage(signature, existingFile);
|
||||
arbitraryDataFile = existingFile;
|
||||
}
|
||||
|
||||
if (arbitraryDataFile == null) {
|
||||
// We don't have a file, so give up here
|
||||
return null;
|
||||
}
|
||||
|
||||
// We might want to forward the request to the peer that originally requested it
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, arbitraryDataFileMessage, originalMessage);
|
||||
this.handleArbitraryDataFileForwarding(requestingPeer, new ArbitraryDataFileMessage(signature, arbitraryDataFile), originalMessage);
|
||||
|
||||
boolean isRelayRequest = (requestingPeer != null);
|
||||
if (isRelayRequest) {
|
||||
if (!fileAlreadyExists) {
|
||||
// File didn't exist locally before the request, and it's a forwarding request, so delete it
|
||||
LOGGER.debug("Deleting file {} because it was needed for forwarding only", Base58.encode(hash));
|
||||
ArbitraryDataFile dataFile = arbitraryDataFileMessage.getArbitraryDataFile();
|
||||
|
||||
// Keep trying to delete the data until it is deleted, or we reach 10 attempts
|
||||
dataFile.delete(10);
|
||||
arbitraryDataFile.delete(10);
|
||||
}
|
||||
}
|
||||
|
||||
return arbitraryDataFileMessage;
|
||||
return arbitraryDataFile;
|
||||
}
|
||||
|
||||
private void handleFileListRequests(byte[] signature) {
|
||||
|
@@ -114,7 +114,7 @@ public class ArbitraryDataFileRequestThread implements Runnable {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.debug("Fetching file {} from peer {} via request thread...", hash58, peer);
|
||||
LOGGER.trace("Fetching file {} from peer {} via request thread...", hash58, peer);
|
||||
arbitraryDataFileManager.fetchArbitraryDataFiles(repository, peer, signature, arbitraryTransactionData, Arrays.asList(hash));
|
||||
|
||||
} catch (DataException e) {
|
||||
|
@@ -27,6 +27,7 @@ import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ListUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class ArbitraryDataManager extends Thread {
|
||||
@@ -172,7 +173,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
|
||||
private void processNames() throws InterruptedException {
|
||||
// Fetch latest list of followed names
|
||||
List<String> followedNames = ResourceListManager.getInstance().getStringsInList("followedNames");
|
||||
List<String> followedNames = ListUtils.followedNames();
|
||||
if (followedNames == null || followedNames.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -398,6 +399,11 @@ public class ArbitraryDataManager extends Thread {
|
||||
// Entrypoint to request new metadata from peers
|
||||
public ArbitraryDataTransactionMetadata fetchMetadata(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
|
||||
if (arbitraryTransactionData.getService() == null) {
|
||||
// Can't fetch metadata without a valid service
|
||||
return null;
|
||||
}
|
||||
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(
|
||||
arbitraryTransactionData.getName(),
|
||||
ArbitraryDataFile.ResourceIdType.NAME,
|
||||
@@ -489,7 +495,7 @@ public class ArbitraryDataManager extends Thread {
|
||||
public void invalidateCache(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
String signature58 = Base58.encode(arbitraryTransactionData.getSignature());
|
||||
|
||||
if (arbitraryTransactionData.getName() != null) {
|
||||
if (arbitraryTransactionData.getName() != null && arbitraryTransactionData.getService() != null) {
|
||||
String resourceId = arbitraryTransactionData.getName().toLowerCase();
|
||||
Service service = arbitraryTransactionData.getService();
|
||||
String identifier = arbitraryTransactionData.getIdentifier();
|
||||
|
@@ -5,15 +5,11 @@ import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.list.ResourceListManager;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.utils.ArbitraryTransactionUtils;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.FilesystemUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
@@ -135,11 +131,11 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
case ALL:
|
||||
case VIEWED:
|
||||
// If the policy includes viewed data, we can host it as long as it's not blocked
|
||||
return !this.isNameBlocked(name);
|
||||
return !ListUtils.isNameBlocked(name);
|
||||
|
||||
case FOLLOWED:
|
||||
// If the policy is for followed data only, we have to be following it
|
||||
return this.isFollowingName(name);
|
||||
return ListUtils.isFollowingName(name);
|
||||
|
||||
// For NONE or all else, we shouldn't host this data
|
||||
case NONE:
|
||||
@@ -188,14 +184,14 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
}
|
||||
|
||||
// Never fetch data from blocked names, even if they are followed
|
||||
if (this.isNameBlocked(name)) {
|
||||
if (ListUtils.isNameBlocked(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (Settings.getInstance().getStoragePolicy()) {
|
||||
case FOLLOWED:
|
||||
case FOLLOWED_OR_VIEWED:
|
||||
return this.isFollowingName(name);
|
||||
return ListUtils.isFollowingName(name);
|
||||
|
||||
case ALL:
|
||||
return true;
|
||||
@@ -235,7 +231,7 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
* @return boolean - whether the resource is blocked or not
|
||||
*/
|
||||
public boolean isBlocked(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
return isNameBlocked(arbitraryTransactionData.getName());
|
||||
return ListUtils.isNameBlocked(arbitraryTransactionData.getName());
|
||||
}
|
||||
|
||||
private boolean isDataTypeAllowed(ArbitraryTransactionData arbitraryTransactionData) {
|
||||
@@ -253,22 +249,6 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isNameBlocked(String name) {
|
||||
return ResourceListManager.getInstance().listContains("blockedNames", name, false);
|
||||
}
|
||||
|
||||
private boolean isFollowingName(String name) {
|
||||
return ResourceListManager.getInstance().listContains("followedNames", name, false);
|
||||
}
|
||||
|
||||
public List<String> followedNames() {
|
||||
return ResourceListManager.getInstance().getStringsInList("followedNames");
|
||||
}
|
||||
|
||||
private int followedNamesCount() {
|
||||
return ResourceListManager.getInstance().getItemCountForList("followedNames");
|
||||
}
|
||||
|
||||
|
||||
public List<ArbitraryTransactionData> loadAllHostedTransactions(Repository repository) {
|
||||
|
||||
@@ -513,7 +493,7 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
return true;
|
||||
}
|
||||
|
||||
int followedNamesCount = this.followedNamesCount();
|
||||
int followedNamesCount = ListUtils.followedNamesCount();
|
||||
if (followedNamesCount == 0) {
|
||||
// Not following any names, so we have space
|
||||
return true;
|
||||
@@ -543,7 +523,7 @@ public class ArbitraryDataStorageManager extends Thread {
|
||||
}
|
||||
|
||||
public long storageCapacityPerName(double threshold) {
|
||||
int followedNamesCount = this.followedNamesCount();
|
||||
int followedNamesCount = ListUtils.followedNamesCount();
|
||||
if (followedNamesCount == 0) {
|
||||
// Not following any names, so we have the total space available
|
||||
return this.getStorageCapacityIncludingThreshold(threshold);
|
||||
|
@@ -16,6 +16,7 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ListUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
import org.qortal.utils.Triple;
|
||||
|
||||
@@ -332,7 +333,7 @@ public class ArbitraryMetadataManager {
|
||||
}
|
||||
|
||||
// Check if the name is blocked
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(arbitraryTransactionData.getName()));
|
||||
boolean isBlocked = (arbitraryTransactionData == null || ListUtils.isNameBlocked(arbitraryTransactionData.getName()));
|
||||
if (!isBlocked) {
|
||||
Peer requestingPeer = request.getB();
|
||||
if (requestingPeer != null) {
|
||||
@@ -420,7 +421,7 @@ public class ArbitraryMetadataManager {
|
||||
}
|
||||
|
||||
// We may need to forward this request on
|
||||
boolean isBlocked = (transactionData == null || ArbitraryDataStorageManager.getInstance().isNameBlocked(transactionData.getName()));
|
||||
boolean isBlocked = (transactionData == null || ListUtils.isNameBlocked(transactionData.getName()));
|
||||
if (Settings.getInstance().isRelayModeEnabled() && !isBlocked) {
|
||||
// In relay mode - so ask our other peers if they have it
|
||||
|
||||
|
@@ -39,9 +39,10 @@ public class AtStatesPruner implements Runnable {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int pruneStartHeight = repository.getATRepository().getAtPruneHeight();
|
||||
int maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
|
||||
|
||||
repository.discardChanges();
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
@@ -92,7 +93,8 @@ public class AtStatesPruner implements Runnable {
|
||||
if (upperPrunableHeight > upperBatchHeight) {
|
||||
pruneStartHeight = upperBatchHeight;
|
||||
repository.getATRepository().setAtPruneHeight(pruneStartHeight);
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
|
||||
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
final int finalPruneStartHeight = pruneStartHeight;
|
||||
|
@@ -26,9 +26,10 @@ public class AtStatesTrimmer implements Runnable {
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int trimStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
int maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
|
||||
|
||||
repository.discardChanges();
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
@@ -70,7 +71,8 @@ public class AtStatesTrimmer implements Runnable {
|
||||
if (upperTrimmableHeight > upperBatchHeight) {
|
||||
trimStartHeight = upperBatchHeight;
|
||||
repository.getATRepository().setAtTrimHeight(trimStartHeight);
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
maxLatestAtStatesHeight = PruneManager.getMaxHeightForLatestAtStates(repository);
|
||||
repository.getATRepository().rebuildLatestAtStates(maxLatestAtStatesHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
final int finalTrimStartHeight = trimStartHeight;
|
||||
|
@@ -0,0 +1,121 @@
|
||||
package org.qortal.controller.repository;
|
||||
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.repository.*;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
|
||||
public class BlockArchiveRebuilder {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveRebuilder.class);
|
||||
|
||||
private final int serializationVersion;
|
||||
|
||||
public BlockArchiveRebuilder(int serializationVersion) {
|
||||
this.serializationVersion = serializationVersion;
|
||||
}
|
||||
|
||||
public void start() throws DataException, IOException {
|
||||
if (!Settings.getInstance().isArchiveEnabled() || Settings.getInstance().isLite()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// New archive path is in a different location from original archive path, to avoid conflicts.
|
||||
// It will be moved later, once the process is complete.
|
||||
final Path newArchivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive-rebuild");
|
||||
final Path originalArchivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive");
|
||||
|
||||
// Delete archive-rebuild if it exists from a previous attempt
|
||||
FileUtils.deleteDirectory(newArchivePath.toFile());
|
||||
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
int startHeight = 1; // We need to rebuild the entire archive
|
||||
|
||||
LOGGER.info("Rebuilding block archive from height {}...", startHeight);
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
repository.discardChanges();
|
||||
|
||||
Thread.sleep(1000L);
|
||||
|
||||
// Don't even attempt if we're mid-sync as our repository requests will be delayed for ages
|
||||
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Rebuild archive
|
||||
try {
|
||||
final int maximumArchiveHeight = BlockArchiveReader.getInstance().getHeightOfLastArchivedBlock();
|
||||
if (startHeight >= maximumArchiveHeight) {
|
||||
// We've finished.
|
||||
// Delete existing archive and move the newly built one into its place
|
||||
FileUtils.deleteDirectory(originalArchivePath.toFile());
|
||||
FileUtils.moveDirectory(newArchivePath.toFile(), originalArchivePath.toFile());
|
||||
BlockArchiveReader.getInstance().invalidateFileListCache();
|
||||
LOGGER.info("Block archive successfully rebuilt");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(startHeight, maximumArchiveHeight, serializationVersion, newArchivePath, repository);
|
||||
|
||||
// Set data source to BLOCK_ARCHIVE as we are rebuilding
|
||||
writer.setDataSource(BlockArchiveWriter.BlockArchiveDataSource.BLOCK_ARCHIVE);
|
||||
|
||||
// We can't enforce the 100MB file size target, as the final file needs to contain all blocks
|
||||
// that exist in the current archive. Otherwise, the final blocks in the archive will be lost.
|
||||
writer.setShouldEnforceFileSizeTarget(false);
|
||||
|
||||
// We want to log the rebuild progress
|
||||
writer.setShouldLogProgress(true);
|
||||
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
switch (result) {
|
||||
case OK:
|
||||
// Increment block archive height
|
||||
startHeight += writer.getWrittenCount();
|
||||
repository.saveChanges();
|
||||
break;
|
||||
|
||||
case STOPPING:
|
||||
return;
|
||||
|
||||
// We've reached the limit of the blocks we can archive
|
||||
// Sleep for a while to allow more to become available
|
||||
case NOT_ENOUGH_BLOCKS:
|
||||
// This shouldn't happen, as we're not enforcing minimum file sizes
|
||||
repository.discardChanges();
|
||||
throw new DataException("Unable to rebuild archive due to unexpected NOT_ENOUGH_BLOCKS response.");
|
||||
|
||||
case BLOCK_NOT_FOUND:
|
||||
// We tried to archive a block that didn't exist. This is a major failure and likely means
|
||||
// that a bootstrap or re-sync is needed. Try again every minute until then.
|
||||
LOGGER.info("Error: block not found when rebuilding archive. If this error persists, " +
|
||||
"a bootstrap or re-sync may be needed.");
|
||||
repository.discardChanges();
|
||||
throw new DataException("Unable to rebuild archive because a block is missing.");
|
||||
}
|
||||
|
||||
} catch (IOException | TransformationException e) {
|
||||
LOGGER.info("Caught exception when rebuilding block archive", e);
|
||||
throw new DataException("Unable to rebuild block archive");
|
||||
}
|
||||
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// Do nothing
|
||||
} finally {
|
||||
// Delete archive-rebuild if it still exists, as that means something went wrong
|
||||
FileUtils.deleteDirectory(newArchivePath.toFile());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -13,7 +13,9 @@ import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Unicode;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class NamesDatabaseIntegrityCheck {
|
||||
|
||||
@@ -28,16 +30,8 @@ public class NamesDatabaseIntegrityCheck {
|
||||
|
||||
private List<TransactionData> nameTransactions = new ArrayList<>();
|
||||
|
||||
|
||||
public int rebuildName(String name, Repository repository) {
|
||||
return this.rebuildName(name, repository, null);
|
||||
}
|
||||
|
||||
public int rebuildName(String name, Repository repository, List<String> referenceNames) {
|
||||
// "referenceNames" tracks the linked names that have already been rebuilt, to prevent circular dependencies
|
||||
if (referenceNames == null) {
|
||||
referenceNames = new ArrayList<>();
|
||||
}
|
||||
|
||||
int modificationCount = 0;
|
||||
try {
|
||||
List<TransactionData> transactions = this.fetchAllTransactionsInvolvingName(name, repository);
|
||||
@@ -46,6 +40,14 @@ public class NamesDatabaseIntegrityCheck {
|
||||
return modificationCount;
|
||||
}
|
||||
|
||||
// If this name has been updated at any point, we need to add transactions from the other names to the sequence
|
||||
int added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository);
|
||||
while (added > 0) {
|
||||
// Keep going until all have been added
|
||||
LOGGER.trace("{} added for {}. Looking for more transactions...", added, name);
|
||||
added = this.addAdditionalTransactionsRelatingToName(transactions, name, repository);
|
||||
}
|
||||
|
||||
// Loop through each past transaction and re-apply it to the Names table
|
||||
for (TransactionData currentTransaction : transactions) {
|
||||
|
||||
@@ -61,29 +63,14 @@ public class NamesDatabaseIntegrityCheck {
|
||||
// Process UPDATE_NAME transactions
|
||||
if (currentTransaction.getType() == TransactionType.UPDATE_NAME) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) currentTransaction;
|
||||
|
||||
if (Objects.equals(updateNameTransactionData.getNewName(), name) &&
|
||||
!Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName())) {
|
||||
// This renames an existing name, so we need to process that instead
|
||||
|
||||
if (!referenceNames.contains(name)) {
|
||||
referenceNames.add(name);
|
||||
this.rebuildName(updateNameTransactionData.getName(), repository, referenceNames);
|
||||
}
|
||||
else {
|
||||
// We've already processed this name so there's nothing more to do
|
||||
}
|
||||
}
|
||||
else {
|
||||
Name nameObj = new Name(repository, name);
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.update(updateNameTransactionData);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name);
|
||||
} else {
|
||||
// Something went wrong
|
||||
throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName()));
|
||||
}
|
||||
Name nameObj = new Name(repository, updateNameTransactionData.getName());
|
||||
if (nameObj != null && nameObj.getNameData() != null) {
|
||||
nameObj.update(updateNameTransactionData);
|
||||
modificationCount++;
|
||||
LOGGER.trace("Processed UPDATE_NAME transaction for name {}", name);
|
||||
} else {
|
||||
// Something went wrong
|
||||
throw new DataException(String.format("Name data not found for name %s", updateNameTransactionData.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,8 +341,8 @@ public class NamesDatabaseIntegrityCheck {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by lowest timestamp first
|
||||
transactions.sort(Comparator.comparingLong(TransactionData::getTimestamp));
|
||||
// Sort by lowest block height first
|
||||
sortTransactions(transactions);
|
||||
|
||||
return transactions;
|
||||
}
|
||||
@@ -419,4 +406,67 @@ public class NamesDatabaseIntegrityCheck {
|
||||
return names;
|
||||
}
|
||||
|
||||
private int addAdditionalTransactionsRelatingToName(List<TransactionData> transactions, String name, Repository repository) throws DataException {
|
||||
int added = 0;
|
||||
|
||||
// If this name has been updated at any point, we need to add transactions from the other names to the sequence
|
||||
List<String> otherNames = new ArrayList<>();
|
||||
List<TransactionData> updateNameTransactions = transactions.stream().filter(t -> t.getType() == TransactionType.UPDATE_NAME).collect(Collectors.toList());
|
||||
for (TransactionData transactionData : updateNameTransactions) {
|
||||
UpdateNameTransactionData updateNameTransactionData = (UpdateNameTransactionData) transactionData;
|
||||
// If the newName field isn't empty, and either the "name" or "newName" is different from our reference name,
|
||||
// we should remember this additional name, in case it has relevant transactions associated with it.
|
||||
if (updateNameTransactionData.getNewName() != null && !updateNameTransactionData.getNewName().isEmpty()) {
|
||||
if (!Objects.equals(updateNameTransactionData.getName(), name)) {
|
||||
otherNames.add(updateNameTransactionData.getName());
|
||||
}
|
||||
if (!Objects.equals(updateNameTransactionData.getNewName(), name)) {
|
||||
otherNames.add(updateNameTransactionData.getNewName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (String otherName : otherNames) {
|
||||
List<TransactionData> otherNameTransactions = this.fetchAllTransactionsInvolvingName(otherName, repository);
|
||||
for (TransactionData otherNameTransactionData : otherNameTransactions) {
|
||||
if (!transactions.contains(otherNameTransactionData)) {
|
||||
// Add new transaction relating to other name
|
||||
transactions.add(otherNameTransactionData);
|
||||
added++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (added > 0) {
|
||||
// New transaction(s) added, so re-sort
|
||||
sortTransactions(transactions);
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
private void sortTransactions(List<TransactionData> transactions) {
|
||||
Collections.sort(transactions, new Comparator() {
|
||||
public int compare(Object o1, Object o2) {
|
||||
TransactionData td1 = (TransactionData) o1;
|
||||
TransactionData td2 = (TransactionData) o2;
|
||||
|
||||
// Sort by block height first
|
||||
int heightComparison = td1.getBlockHeight().compareTo(td2.getBlockHeight());
|
||||
if (heightComparison != 0) {
|
||||
return heightComparison;
|
||||
}
|
||||
|
||||
// Same height so compare timestamps
|
||||
int timestampComparison = Long.compare(td1.getTimestamp(), td2.getTimestamp());
|
||||
if (timestampComparison != 0) {
|
||||
return timestampComparison;
|
||||
}
|
||||
|
||||
// Same timestamp so compare signatures
|
||||
return new BigInteger(td1.getSignature()).compareTo(new BigInteger(td2.getSignature()));
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -157,4 +157,18 @@ public class PruneManager {
|
||||
return (height < latestUnprunedHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* When rebuilding the latest AT states, we need to specify a maxHeight, so that we aren't tracking
|
||||
* very recent AT states that could potentially be orphaned. This method ensures that AT states
|
||||
* are given a sufficient number of blocks to confirm before being tracked as a latest AT state.
|
||||
*/
|
||||
public static int getMaxHeightForLatestAtStates(Repository repository) throws DataException {
|
||||
// Get current chain height, and subtract a certain number of "confirmation" blocks
|
||||
// This is to ensure we are basing our latest AT states data on confirmed blocks -
|
||||
// ones that won't be orphaned in any normal circumstances
|
||||
final int confirmationBlocks = 250;
|
||||
final int chainHeight = repository.getBlockRepository().getBlockchainHeight();
|
||||
return chainHeight - confirmationBlocks;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -45,6 +45,7 @@ public class Digibyte extends Bitcoiny {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=dgb
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 55002),
|
||||
new Server("electrum-dgb.qortal.online", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1-dgb.qortal.online", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20059),
|
||||
|
@@ -46,6 +46,7 @@ public class Dogecoin extends Bitcoiny {
|
||||
return Arrays.asList(
|
||||
// Servers chosen on NO BASIS WHATSOEVER from various sources!
|
||||
// Status verified at https://1209k.com/bitcoin-eye/ele.php?chain=doge
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 54002),
|
||||
new Server("electrum-doge.qortal.online", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1-doge.qortal.online", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20060),
|
||||
|
@@ -50,6 +50,7 @@ public class Litecoin extends Bitcoiny {
|
||||
//BEHIND new Server("62.171.169.176", Server.ConnectionType.SSL, 50002),
|
||||
//PHISHY new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002),
|
||||
new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443),
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002),
|
||||
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
|
||||
new Server("electrum-ltc.qortal.online", Server.ConnectionType.SSL, 50002),
|
||||
|
@@ -48,6 +48,7 @@ public class Ravencoin extends Bitcoiny {
|
||||
//CLOSED new Server("aethyn.com", ConnectionType.SSL, 50002),
|
||||
//CLOSED new Server("electrum2.rvn.rocks", ConnectionType.SSL, 50002),
|
||||
//BEHIND new Server("electrum3.rvn.rocks", ConnectionType.SSL, 50002),
|
||||
new Server("electrum.qortal.link", Server.ConnectionType.SSL, 56002),
|
||||
new Server("electrum-rvn.qortal.online", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1-rvn.qortal.online", ConnectionType.SSL, 50002),
|
||||
new Server("electrum1.cipig.net", ConnectionType.SSL, 20051),
|
||||
|
@@ -202,4 +202,12 @@ public class AES {
|
||||
.decode(cipherText)));
|
||||
}
|
||||
|
||||
public static long getEncryptedFileSize(long inFileSize) {
|
||||
// To calculate the resulting file size, add 16 (for the IV), then round up to the nearest multiple of 16
|
||||
final int ivSize = 16;
|
||||
final int chunkSize = 16;
|
||||
final int expectedSize = Math.round((inFileSize + ivSize) / chunkSize) * chunkSize + chunkSize;
|
||||
return expectedSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -16,10 +16,17 @@ public class ArbitraryResourceInfo {
|
||||
public ArbitraryResourceMetadata metadata;
|
||||
|
||||
public Long size;
|
||||
public Long created;
|
||||
public Long updated;
|
||||
|
||||
public ArbitraryResourceInfo() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return String.format("%s %s %s", name, service, identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (o == this)
|
||||
|
@@ -16,16 +16,18 @@ public class ArbitraryResourceMetadata {
|
||||
private Category category;
|
||||
private String categoryName;
|
||||
private List<String> files;
|
||||
private String mimeType;
|
||||
|
||||
public ArbitraryResourceMetadata() {
|
||||
}
|
||||
|
||||
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category, List<String> files) {
|
||||
public ArbitraryResourceMetadata(String title, String description, List<String> tags, Category category, List<String> files, String mimeType) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.tags = tags;
|
||||
this.category = category;
|
||||
this.files = files;
|
||||
this.mimeType = mimeType;
|
||||
|
||||
if (category != null) {
|
||||
this.categoryName = category.getName();
|
||||
@@ -40,6 +42,7 @@ public class ArbitraryResourceMetadata {
|
||||
String description = transactionMetadata.getDescription();
|
||||
List<String> tags = transactionMetadata.getTags();
|
||||
Category category = transactionMetadata.getCategory();
|
||||
String mimeType = transactionMetadata.getMimeType();
|
||||
|
||||
// We don't always want to include the file list as it can be too verbose
|
||||
List<String> files = null;
|
||||
@@ -47,11 +50,11 @@ public class ArbitraryResourceMetadata {
|
||||
files = transactionMetadata.getFiles();
|
||||
}
|
||||
|
||||
if (title == null && description == null && tags == null && category == null && files == null) {
|
||||
if (title == null && description == null && tags == null && category == null && files == null && mimeType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ArbitraryResourceMetadata(title, description, tags, category, files);
|
||||
return new ArbitraryResourceMetadata(title, description, tags, category, files, mimeType);
|
||||
}
|
||||
|
||||
public List<String> getFiles() {
|
||||
|
@@ -8,6 +8,7 @@ public class ArbitraryResourceStatus {
|
||||
|
||||
public enum Status {
|
||||
PUBLISHED("Published", "Published but not yet downloaded"),
|
||||
NOT_PUBLISHED("Not published", "Resource does not exist"),
|
||||
DOWNLOADING("Downloading", "Locating and downloading files..."),
|
||||
DOWNLOADED("Downloaded", "Files downloaded"),
|
||||
BUILDING("Building", "Building..."),
|
||||
@@ -33,6 +34,7 @@ public class ArbitraryResourceStatus {
|
||||
|
||||
private Integer localChunkCount;
|
||||
private Integer totalChunkCount;
|
||||
private Float percentLoaded;
|
||||
|
||||
public ArbitraryResourceStatus() {
|
||||
}
|
||||
@@ -44,6 +46,7 @@ public class ArbitraryResourceStatus {
|
||||
this.description = status.description;
|
||||
this.localChunkCount = localChunkCount;
|
||||
this.totalChunkCount = totalChunkCount;
|
||||
this.percentLoaded = (this.localChunkCount != null && this.totalChunkCount != null) ? this.localChunkCount / (float)this.totalChunkCount * 100.0f : null;
|
||||
}
|
||||
|
||||
public ArbitraryResourceStatus(Status status) {
|
||||
|
@@ -1,10 +1,15 @@
|
||||
package org.qortal.data.chat;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ActiveChats {
|
||||
|
||||
@@ -17,17 +22,39 @@ public class ActiveChats {
|
||||
private Long timestamp;
|
||||
private String sender;
|
||||
private String senderName;
|
||||
private byte[] signature;
|
||||
private Encoding encoding;
|
||||
private String data;
|
||||
|
||||
protected GroupChat() {
|
||||
/* JAXB */
|
||||
}
|
||||
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName) {
|
||||
public GroupChat(int groupId, String groupName, Long timestamp, String sender, String senderName,
|
||||
byte[] signature, Encoding encoding, byte[] data) {
|
||||
this.groupId = groupId;
|
||||
this.groupName = groupName;
|
||||
this.timestamp = timestamp;
|
||||
this.sender = sender;
|
||||
this.senderName = senderName;
|
||||
this.signature = signature;
|
||||
this.encoding = encoding != null ? encoding : Encoding.BASE58;
|
||||
|
||||
if (data != null) {
|
||||
switch (this.encoding) {
|
||||
case BASE64:
|
||||
this.data = Base64.toBase64String(data);
|
||||
break;
|
||||
|
||||
case BASE58:
|
||||
default:
|
||||
this.data = Base58.encode(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.data = null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getGroupId() {
|
||||
@@ -49,6 +76,14 @@ public class ActiveChats {
|
||||
public String getSenderName() {
|
||||
return this.senderName;
|
||||
}
|
||||
|
||||
public byte[] getSignature() {
|
||||
return this.signature;
|
||||
}
|
||||
|
||||
public String getData() {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@@ -118,4 +153,4 @@ public class ActiveChats {
|
||||
return this.direct;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,11 +1,19 @@
|
||||
package org.qortal.data.chat;
|
||||
|
||||
import org.bouncycastle.util.encoders.Base64;
|
||||
import org.qortal.utils.Base58;
|
||||
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
public class ChatMessage {
|
||||
|
||||
public enum Encoding {
|
||||
BASE58,
|
||||
BASE64
|
||||
}
|
||||
|
||||
// Properties
|
||||
|
||||
private long timestamp;
|
||||
@@ -29,7 +37,9 @@ public class ChatMessage {
|
||||
|
||||
private byte[] chatReference;
|
||||
|
||||
private byte[] data;
|
||||
private Encoding encoding;
|
||||
|
||||
private String data;
|
||||
|
||||
private boolean isText;
|
||||
private boolean isEncrypted;
|
||||
@@ -44,8 +54,8 @@ public class ChatMessage {
|
||||
|
||||
// For repository use
|
||||
public ChatMessage(long timestamp, int txGroupId, byte[] reference, byte[] senderPublicKey, String sender,
|
||||
String senderName, String recipient, String recipientName, byte[] chatReference, byte[] data,
|
||||
boolean isText, boolean isEncrypted, byte[] signature) {
|
||||
String senderName, String recipient, String recipientName, byte[] chatReference,
|
||||
Encoding encoding, byte[] data, boolean isText, boolean isEncrypted, byte[] signature) {
|
||||
this.timestamp = timestamp;
|
||||
this.txGroupId = txGroupId;
|
||||
this.reference = reference;
|
||||
@@ -55,7 +65,24 @@ public class ChatMessage {
|
||||
this.recipient = recipient;
|
||||
this.recipientName = recipientName;
|
||||
this.chatReference = chatReference;
|
||||
this.data = data;
|
||||
this.encoding = encoding != null ? encoding : Encoding.BASE58;
|
||||
|
||||
if (data != null) {
|
||||
switch (this.encoding) {
|
||||
case BASE64:
|
||||
this.data = Base64.toBase64String(data);
|
||||
break;
|
||||
|
||||
case BASE58:
|
||||
default:
|
||||
this.data = Base58.encode(data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.data = null;
|
||||
}
|
||||
|
||||
this.isText = isText;
|
||||
this.isEncrypted = isEncrypted;
|
||||
this.signature = signature;
|
||||
@@ -97,7 +124,7 @@ public class ChatMessage {
|
||||
return this.chatReference;
|
||||
}
|
||||
|
||||
public byte[] getData() {
|
||||
public String getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
|
@@ -73,7 +73,7 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
@Schema(example = "sender_public_key")
|
||||
private byte[] senderPublicKey;
|
||||
|
||||
private Service service;
|
||||
private int service;
|
||||
private int nonce;
|
||||
private int size;
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public ArbitraryTransactionData(BaseTransactionData baseTransactionData,
|
||||
int version, Service service, int nonce, int size,
|
||||
int version, int service, int nonce, int size,
|
||||
String name, String identifier, Method method, byte[] secret, Compression compression,
|
||||
byte[] data, DataType dataType, byte[] metadataHash, List<PaymentData> payments) {
|
||||
super(TransactionType.ARBITRARY, baseTransactionData);
|
||||
@@ -135,6 +135,10 @@ public class ArbitraryTransactionData extends TransactionData {
|
||||
}
|
||||
|
||||
public Service getService() {
|
||||
return Service.valueOf(this.service);
|
||||
}
|
||||
|
||||
public int getServiceInt() {
|
||||
return this.service;
|
||||
}
|
||||
|
||||
|
@@ -2,9 +2,11 @@ package org.qortal.data.transaction;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
|
||||
import org.qortal.data.voting.PollOptionData;
|
||||
import org.qortal.transaction.Transaction;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@@ -14,8 +16,13 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
@XmlDiscriminatorValue("CREATE_POLL")
|
||||
public class CreatePollTransactionData extends TransactionData {
|
||||
|
||||
|
||||
@Schema(description = "Poll creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
private byte[] pollCreatorPublicKey;
|
||||
|
||||
// Properties
|
||||
private String owner;
|
||||
private String pollName;
|
||||
@@ -29,10 +36,15 @@ public class CreatePollTransactionData extends TransactionData {
|
||||
super(TransactionType.CREATE_POLL);
|
||||
}
|
||||
|
||||
public void afterUnmarshal(Unmarshaller u, Object parent) {
|
||||
this.creatorPublicKey = this.pollCreatorPublicKey;
|
||||
}
|
||||
|
||||
public CreatePollTransactionData(BaseTransactionData baseTransactionData,
|
||||
String owner, String pollName, String description, List<PollOptionData> pollOptions) {
|
||||
super(Transaction.TransactionType.CREATE_POLL, baseTransactionData);
|
||||
|
||||
this.creatorPublicKey = baseTransactionData.creatorPublicKey;
|
||||
this.owner = owner;
|
||||
this.pollName = pollName;
|
||||
this.description = description;
|
||||
@@ -41,6 +53,7 @@ public class CreatePollTransactionData extends TransactionData {
|
||||
|
||||
// Getters/setters
|
||||
|
||||
public byte[] getPollCreatorPublicKey() { return this.creatorPublicKey; }
|
||||
public String getOwner() {
|
||||
return this.owner;
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorNode;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.voting.PollData;
|
||||
import org.qortal.transaction.Transaction.ApprovalStatus;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
@@ -29,6 +30,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
|
||||
@XmlSeeAlso({GenesisTransactionData.class, PaymentTransactionData.class, RegisterNameTransactionData.class, UpdateNameTransactionData.class,
|
||||
SellNameTransactionData.class, CancelSellNameTransactionData.class, BuyNameTransactionData.class,
|
||||
CreatePollTransactionData.class, VoteOnPollTransactionData.class, ArbitraryTransactionData.class,
|
||||
PollData.class,
|
||||
IssueAssetTransactionData.class, TransferAssetTransactionData.class,
|
||||
CreateAssetOrderTransactionData.class, CancelAssetOrderTransactionData.class,
|
||||
MultiPaymentTransactionData.class, DeployAtTransactionData.class, MessageTransactionData.class, ATTransactionData.class,
|
||||
|
@@ -3,7 +3,9 @@ package org.qortal.data.transaction;
|
||||
import javax.xml.bind.Unmarshaller;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlTransient;
|
||||
|
||||
import org.eclipse.persistence.oxm.annotations.XmlDiscriminatorValue;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
@@ -11,12 +13,17 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
// All properties to be converted to JSON via JAXB
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@Schema(allOf = { TransactionData.class })
|
||||
@XmlDiscriminatorValue("VOTE_ON_POLL")
|
||||
public class VoteOnPollTransactionData extends TransactionData {
|
||||
|
||||
// Properties
|
||||
@Schema(description = "Vote creator's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
|
||||
private byte[] voterPublicKey;
|
||||
private String pollName;
|
||||
private int optionIndex;
|
||||
// For internal use when orphaning
|
||||
@XmlTransient
|
||||
@Schema(hidden = true)
|
||||
private Integer previousOptionIndex;
|
||||
|
||||
// Constructors
|
||||
|
@@ -14,6 +14,11 @@ public class PollData {
|
||||
|
||||
// Constructors
|
||||
|
||||
// For JAXB
|
||||
protected PollData() {
|
||||
super();
|
||||
}
|
||||
|
||||
public PollData(byte[] creatorPublicKey, String owner, String pollName, String description, List<PollOptionData> pollOptions, long published) {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
this.owner = owner;
|
||||
@@ -29,22 +34,42 @@ public class PollData {
|
||||
return this.creatorPublicKey;
|
||||
}
|
||||
|
||||
public void setCreatorPublicKey(byte[] creatorPublicKey) {
|
||||
this.creatorPublicKey = creatorPublicKey;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return this.owner;
|
||||
}
|
||||
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public String getPollName() {
|
||||
return this.pollName;
|
||||
}
|
||||
|
||||
public void setPollName(String pollName) {
|
||||
this.pollName = pollName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public List<PollOptionData> getPollOptions() {
|
||||
return this.pollOptions;
|
||||
}
|
||||
|
||||
public void setPollOptions(List<PollOptionData> pollOptions) {
|
||||
this.pollOptions = pollOptions;
|
||||
}
|
||||
|
||||
public long getPublished() {
|
||||
return this.published;
|
||||
}
|
||||
|
@@ -52,6 +52,15 @@ public class ResourceList {
|
||||
String jsonString = ResourceList.listToJSONString(this.list);
|
||||
Path filePath = this.getFilePath();
|
||||
|
||||
// Don't create list if it's empty
|
||||
if (this.list != null && this.list.isEmpty()) {
|
||||
if (filePath != null && Files.exists(filePath)) {
|
||||
// Delete empty list
|
||||
Files.delete(filePath);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create parent directory if needed
|
||||
try {
|
||||
Files.createDirectories(filePath.getParent());
|
||||
@@ -109,6 +118,13 @@ public class ResourceList {
|
||||
this.list.remove(resource);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
if (this.list == null) {
|
||||
return;
|
||||
}
|
||||
this.list.clear();
|
||||
}
|
||||
|
||||
public boolean contains(String resource, boolean caseSensitive) {
|
||||
if (resource == null || this.list == null) {
|
||||
return false;
|
||||
|
@@ -2,8 +2,11 @@ package org.qortal.list;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -18,6 +21,7 @@ public class ResourceListManager {
|
||||
|
||||
|
||||
public ResourceListManager() {
|
||||
this.lists = this.fetchLists();
|
||||
}
|
||||
|
||||
public static synchronized ResourceListManager getInstance() {
|
||||
@@ -27,6 +31,38 @@ public class ResourceListManager {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static synchronized void reset() {
|
||||
if (instance != null) {
|
||||
instance = null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<ResourceList> fetchLists() {
|
||||
List<ResourceList> lists = new ArrayList<>();
|
||||
Path listsPath = Paths.get(Settings.getInstance().getListsPath());
|
||||
|
||||
if (listsPath.toFile().isDirectory()) {
|
||||
String[] files = listsPath.toFile().list();
|
||||
|
||||
for (String fileName : files) {
|
||||
try {
|
||||
// Remove .json extension
|
||||
if (fileName.endsWith(".json")) {
|
||||
fileName = fileName.substring(0, fileName.length() - 5);
|
||||
}
|
||||
|
||||
ResourceList list = new ResourceList(fileName);
|
||||
if (list != null) {
|
||||
lists.add(list);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Ignore this list
|
||||
}
|
||||
}
|
||||
}
|
||||
return lists;
|
||||
}
|
||||
|
||||
private ResourceList getList(String listName) {
|
||||
for (ResourceList list : this.lists) {
|
||||
if (Objects.equals(list.getName(), listName)) {
|
||||
@@ -48,6 +84,18 @@ public class ResourceListManager {
|
||||
|
||||
}
|
||||
|
||||
private List<ResourceList> getListsByPrefix(String listNamePrefix) {
|
||||
List<ResourceList> lists = new ArrayList<>();
|
||||
|
||||
for (ResourceList list : this.lists) {
|
||||
if (list != null && list.getName() != null && list.getName().startsWith(listNamePrefix)) {
|
||||
lists.add(list);
|
||||
}
|
||||
}
|
||||
|
||||
return lists;
|
||||
}
|
||||
|
||||
public boolean addToList(String listName, String item, boolean save) {
|
||||
ResourceList list = this.getList(listName);
|
||||
if (list == null) {
|
||||
@@ -95,6 +143,16 @@ public class ResourceListManager {
|
||||
return list.contains(item, caseSensitive);
|
||||
}
|
||||
|
||||
public boolean listWithPrefixContains(String listNamePrefix, String item, boolean caseSensitive) {
|
||||
List<ResourceList> lists = getListsByPrefix(listNamePrefix);
|
||||
for (ResourceList list : lists) {
|
||||
if (list.contains(item, caseSensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void saveList(String listName) {
|
||||
ResourceList list = this.getList(listName);
|
||||
if (list == null) {
|
||||
@@ -133,6 +191,15 @@ public class ResourceListManager {
|
||||
return list.getList();
|
||||
}
|
||||
|
||||
public List<String> getStringsInListsWithPrefix(String listNamePrefix) {
|
||||
List<String> items = new ArrayList<>();
|
||||
List<ResourceList> lists = getListsByPrefix(listNamePrefix);
|
||||
for (ResourceList list : lists) {
|
||||
items.addAll(list.getList());
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public int getItemCountForList(String listName) {
|
||||
ResourceList list = this.getList(listName);
|
||||
if (list == null) {
|
||||
|
@@ -16,6 +16,8 @@ import org.qortal.repository.Repository;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
import org.qortal.utils.Unicode;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Name {
|
||||
|
||||
// Properties
|
||||
@@ -116,7 +118,7 @@ public class Name {
|
||||
|
||||
this.repository.getNameRepository().save(this.nameData);
|
||||
|
||||
if (!updateNameTransactionData.getNewName().isEmpty())
|
||||
if (!updateNameTransactionData.getNewName().isEmpty() && !Objects.equals(updateNameTransactionData.getName(), updateNameTransactionData.getNewName()))
|
||||
// Name has changed, delete old entry
|
||||
this.repository.getNameRepository().delete(updateNameTransactionData.getNewName());
|
||||
|
||||
|
@@ -124,6 +124,8 @@ public class Network {
|
||||
|
||||
private final List<PeerAddress> selfPeers = new ArrayList<>();
|
||||
|
||||
private String bindAddress = null;
|
||||
|
||||
private final ExecuteProduceConsume networkEPC;
|
||||
private Selector channelSelector;
|
||||
private ServerSocketChannel serverChannel;
|
||||
@@ -159,25 +161,43 @@ public class Network {
|
||||
// Grab P2P port from settings
|
||||
int listenPort = Settings.getInstance().getListenPort();
|
||||
|
||||
// Grab P2P bind address from settings
|
||||
try {
|
||||
InetAddress bindAddr = InetAddress.getByName(Settings.getInstance().getBindAddress());
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, listenPort);
|
||||
// Grab P2P bind addresses from settings
|
||||
List<String> bindAddresses = new ArrayList<>();
|
||||
if (Settings.getInstance().getBindAddress() != null) {
|
||||
bindAddresses.add(Settings.getInstance().getBindAddress());
|
||||
}
|
||||
if (Settings.getInstance().getBindAddressFallback() != null) {
|
||||
bindAddresses.add(Settings.getInstance().getBindAddressFallback());
|
||||
}
|
||||
|
||||
channelSelector = Selector.open();
|
||||
for (int i=0; i<bindAddresses.size(); i++) {
|
||||
try {
|
||||
String bindAddress = bindAddresses.get(i);
|
||||
InetAddress bindAddr = InetAddress.getByName(bindAddress);
|
||||
InetSocketAddress endpoint = new InetSocketAddress(bindAddr, listenPort);
|
||||
|
||||
// Set up listen socket
|
||||
serverChannel = ServerSocketChannel.open();
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
|
||||
serverChannel.bind(endpoint, LISTEN_BACKLOG);
|
||||
serverSelectionKey = serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||
} catch (UnknownHostException e) {
|
||||
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
|
||||
throw new IOException("Can't bind listen socket to address", e);
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Can't create listen socket: {}", e.getMessage());
|
||||
throw new IOException("Can't create listen socket", e);
|
||||
channelSelector = Selector.open();
|
||||
|
||||
// Set up listen socket
|
||||
serverChannel = ServerSocketChannel.open();
|
||||
serverChannel.configureBlocking(false);
|
||||
serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
|
||||
serverChannel.bind(endpoint, LISTEN_BACKLOG);
|
||||
serverSelectionKey = serverChannel.register(channelSelector, SelectionKey.OP_ACCEPT);
|
||||
|
||||
this.bindAddress = bindAddress; // Store the selected address, so that it can be used by other parts of the app
|
||||
break; // We don't want to bind to more than one address
|
||||
} catch (UnknownHostException e) {
|
||||
LOGGER.error("Can't bind listen socket to address {}", Settings.getInstance().getBindAddress());
|
||||
if (i == bindAddresses.size()-1) { // Only throw an exception if all addresses have been tried
|
||||
throw new IOException("Can't bind listen socket to address", e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Can't create listen socket: {}", e.getMessage());
|
||||
if (i == bindAddresses.size()-1) { // Only throw an exception if all addresses have been tried
|
||||
throw new IOException("Can't create listen socket", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load all known peers from repository
|
||||
@@ -228,6 +248,10 @@ public class Network {
|
||||
return this.maxPeers;
|
||||
}
|
||||
|
||||
public String getBindAddress() {
|
||||
return this.bindAddress;
|
||||
}
|
||||
|
||||
public byte[] getMessageMagic() {
|
||||
return Settings.getInstance().isTestNet() ? TESTNET_MESSAGE_MAGIC : MAINNET_MESSAGE_MAGIC;
|
||||
}
|
||||
@@ -1556,7 +1580,7 @@ public class Network {
|
||||
this.isShuttingDown = true;
|
||||
|
||||
// Close listen socket to prevent more incoming connections
|
||||
if (this.serverChannel.isOpen()) {
|
||||
if (this.serverChannel != null && this.serverChannel.isOpen()) {
|
||||
try {
|
||||
this.serverChannel.close();
|
||||
} catch (IOException e) {
|
||||
|
@@ -0,0 +1,43 @@
|
||||
package org.qortal.network.message;
|
||||
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
// This is an OUTGOING-only Message which more readily lends itself to being cached
|
||||
public class CachedBlockV2Message extends Message implements Cloneable {
|
||||
|
||||
public CachedBlockV2Message(Block block) throws TransformationException {
|
||||
super(MessageType.BLOCK_V2);
|
||||
|
||||
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
|
||||
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
|
||||
bytes.write(BlockTransformer.toBytes(block));
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("IOException shouldn't occur with ByteArrayOutputStream");
|
||||
}
|
||||
|
||||
this.dataBytes = bytes.toByteArray();
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public CachedBlockV2Message(byte[] cachedBytes) {
|
||||
super(MessageType.BLOCK_V2);
|
||||
|
||||
this.dataBytes = cachedBytes;
|
||||
this.checksumBytes = Message.generateChecksum(this.dataBytes);
|
||||
}
|
||||
|
||||
public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) {
|
||||
throw new UnsupportedOperationException("CachedBlockMessageV2 is for outgoing messages only");
|
||||
}
|
||||
|
||||
}
|
@@ -119,7 +119,7 @@ public interface ATRepository {
|
||||
* <p>
|
||||
* NOTE: performs implicit <tt>repository.saveChanges()</tt>.
|
||||
*/
|
||||
public void rebuildLatestAtStates() throws DataException;
|
||||
public void rebuildLatestAtStates(int maxHeight) throws DataException;
|
||||
|
||||
|
||||
/** Returns height of first trimmable AT state. */
|
||||
|
@@ -24,9 +24,9 @@ public interface ArbitraryRepository {
|
||||
public ArbitraryTransactionData getLatestTransaction(String name, Service service, Method method, String identifier) throws DataException;
|
||||
|
||||
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, List<String> names, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, List<String> names, boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query, String identifier, List<String> names, boolean prefixOnly, List<String> namesFilter, boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public List<ArbitraryResourceNameInfo> getArbitraryResourceCreatorNames(Service service, String identifier, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
|
@@ -3,10 +3,7 @@ package org.qortal.repository;
|
||||
import com.google.common.primitives.Ints;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
@@ -67,20 +64,51 @@ public class BlockArchiveReader {
|
||||
this.fileListCache = Map.copyOf(map);
|
||||
}
|
||||
|
||||
public Integer fetchSerializationVersionForHeight(int height) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
Triple<byte[], Integer, Integer> serializedBlock = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBlock == null) {
|
||||
return null;
|
||||
}
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
return serializationVersion;
|
||||
}
|
||||
|
||||
public BlockTransformation fetchBlockAtHeight(int height) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
byte[] serializedBytes = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBytes == null) {
|
||||
Triple<byte[], Integer, Integer> serializedBlock = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBlock == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] serializedBytes = serializedBlock.getA();
|
||||
Integer serializationVersion = serializedBlock.getB();
|
||||
if (serializedBytes == null || serializationVersion == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ByteBuffer byteBuffer = ByteBuffer.wrap(serializedBytes);
|
||||
BlockTransformation blockInfo = null;
|
||||
try {
|
||||
blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
switch (serializationVersion) {
|
||||
case 1:
|
||||
blockInfo = BlockTransformer.fromByteBuffer(byteBuffer);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
blockInfo = BlockTransformer.fromByteBufferV2(byteBuffer);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Invalid serialization version
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockInfo != null && blockInfo.getBlockData() != null) {
|
||||
// Block height is stored outside of the main serialized bytes, so it
|
||||
// won't be set automatically.
|
||||
@@ -168,15 +196,20 @@ public class BlockArchiveReader {
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] fetchSerializedBlockBytesForSignature(byte[] signature, boolean includeHeightPrefix, Repository repository) {
|
||||
public Triple<byte[], Integer, Integer> fetchSerializedBlockBytesForSignature(byte[] signature, boolean includeHeightPrefix, Repository repository) {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
Integer height = this.fetchHeightForSignature(signature, repository);
|
||||
if (height != null) {
|
||||
byte[] blockBytes = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (blockBytes == null) {
|
||||
Triple<byte[], Integer, Integer> serializedBlock = this.fetchSerializedBlockBytesForHeight(height);
|
||||
if (serializedBlock == null) {
|
||||
return null;
|
||||
}
|
||||
byte[] blockBytes = serializedBlock.getA();
|
||||
Integer version = serializedBlock.getB();
|
||||
if (blockBytes == null || version == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -187,18 +220,18 @@ public class BlockArchiveReader {
|
||||
try {
|
||||
bytes.write(Ints.toByteArray(height));
|
||||
bytes.write(blockBytes);
|
||||
return bytes.toByteArray();
|
||||
return new Triple<>(bytes.toByteArray(), version, height);
|
||||
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return blockBytes;
|
||||
return new Triple<>(blockBytes, version, height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public byte[] fetchSerializedBlockBytesForHeight(int height) {
|
||||
public Triple<byte[], Integer, Integer> fetchSerializedBlockBytesForHeight(int height) {
|
||||
String filename = this.getFilenameForHeight(height);
|
||||
if (filename == null) {
|
||||
// We don't have this block in the archive
|
||||
@@ -221,7 +254,7 @@ public class BlockArchiveReader {
|
||||
// End of fixed length header
|
||||
|
||||
// Make sure the version is one we recognize
|
||||
if (version != 1) {
|
||||
if (version != 1 && version != 2) {
|
||||
LOGGER.info("Error: unknown version in file {}: {}", filename, version);
|
||||
return null;
|
||||
}
|
||||
@@ -258,7 +291,7 @@ public class BlockArchiveReader {
|
||||
byte[] blockBytes = new byte[blockLength];
|
||||
file.read(blockBytes);
|
||||
|
||||
return blockBytes;
|
||||
return new Triple<>(blockBytes, version, height);
|
||||
|
||||
} catch (FileNotFoundException e) {
|
||||
LOGGER.info("File {} not found: {}", filename, e.getMessage());
|
||||
@@ -279,6 +312,30 @@ public class BlockArchiveReader {
|
||||
}
|
||||
}
|
||||
|
||||
public int getHeightOfLastArchivedBlock() {
|
||||
if (this.fileListCache == null) {
|
||||
this.fetchFileList();
|
||||
}
|
||||
|
||||
int maxEndHeight = 0;
|
||||
|
||||
Iterator it = this.fileListCache.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry pair = (Map.Entry) it.next();
|
||||
if (pair == null && pair.getKey() == null && pair.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
Triple<Integer, Integer, Integer> heightInfo = (Triple<Integer, Integer, Integer>) pair.getValue();
|
||||
Integer endHeight = heightInfo.getB();
|
||||
|
||||
if (endHeight != null && endHeight > maxEndHeight) {
|
||||
maxEndHeight = endHeight;
|
||||
}
|
||||
}
|
||||
|
||||
return maxEndHeight;
|
||||
}
|
||||
|
||||
public void invalidateFileListCache() {
|
||||
this.fileListCache = null;
|
||||
}
|
||||
|
@@ -6,10 +6,13 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.block.Block;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.Synchronizer;
|
||||
import org.qortal.data.at.ATStateData;
|
||||
import org.qortal.data.block.BlockArchiveData;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.block.BlockTransformation;
|
||||
import org.qortal.transform.block.BlockTransformer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
@@ -18,6 +21,7 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
|
||||
public class BlockArchiveWriter {
|
||||
|
||||
@@ -28,25 +32,78 @@ public class BlockArchiveWriter {
|
||||
BLOCK_NOT_FOUND
|
||||
}
|
||||
|
||||
public enum BlockArchiveDataSource {
|
||||
BLOCK_REPOSITORY, // To build an archive from the Blocks table
|
||||
BLOCK_ARCHIVE // To build a new archive from an existing archive
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(BlockArchiveWriter.class);
|
||||
|
||||
public static final long DEFAULT_FILE_SIZE_TARGET = 100 * 1024 * 1024; // 100MiB
|
||||
public static final long DEFAULT_FILE_SIZE_TARGET_V1 = 100 * 1024 * 1024; // 100MiB
|
||||
public static final long DEFAULT_FILE_SIZE_TARGET_V2 = 10 * 1024 * 1024; // 10MiB
|
||||
|
||||
private int startHeight;
|
||||
private final int endHeight;
|
||||
private final Integer serializationVersion;
|
||||
private final Path archivePath;
|
||||
private final Repository repository;
|
||||
|
||||
private long fileSizeTarget = DEFAULT_FILE_SIZE_TARGET;
|
||||
private long fileSizeTarget = DEFAULT_FILE_SIZE_TARGET_V1;
|
||||
private boolean shouldEnforceFileSizeTarget = true;
|
||||
|
||||
// Default data source to BLOCK_REPOSITORY; can optionally be overridden
|
||||
private BlockArchiveDataSource dataSource = BlockArchiveDataSource.BLOCK_REPOSITORY;
|
||||
|
||||
private boolean shouldLogProgress = false;
|
||||
|
||||
private int writtenCount;
|
||||
private int lastWrittenHeight;
|
||||
private Path outputPath;
|
||||
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Repository repository) {
|
||||
/**
|
||||
* Instantiate a BlockArchiveWriter using a custom archive path
|
||||
* @param startHeight
|
||||
* @param endHeight
|
||||
* @param repository
|
||||
*/
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Integer serializationVersion, Path archivePath, Repository repository) {
|
||||
this.startHeight = startHeight;
|
||||
this.endHeight = endHeight;
|
||||
this.archivePath = archivePath.toAbsolutePath();
|
||||
this.repository = repository;
|
||||
|
||||
if (serializationVersion == null) {
|
||||
// When serialization version isn't specified, fetch it from the existing archive
|
||||
serializationVersion = this.findSerializationVersion();
|
||||
}
|
||||
|
||||
// Reduce default file size target if we're using V2, as the average block size is over 90% smaller
|
||||
if (serializationVersion == 2) {
|
||||
this.setFileSizeTarget(DEFAULT_FILE_SIZE_TARGET_V2);
|
||||
}
|
||||
|
||||
this.serializationVersion = serializationVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a BlockArchiveWriter using the default archive path and version
|
||||
* @param startHeight
|
||||
* @param endHeight
|
||||
* @param repository
|
||||
*/
|
||||
public BlockArchiveWriter(int startHeight, int endHeight, Repository repository) {
|
||||
this(startHeight, endHeight, null, Paths.get(Settings.getInstance().getRepositoryPath(), "archive"), repository);
|
||||
}
|
||||
|
||||
private int findSerializationVersion() {
|
||||
// Attempt to fetch the serialization version from the existing archive
|
||||
Integer block2SerializationVersion = BlockArchiveReader.getInstance().fetchSerializationVersionForHeight(2);
|
||||
if (block2SerializationVersion != null) {
|
||||
return block2SerializationVersion;
|
||||
}
|
||||
|
||||
// Default to version specified in settings
|
||||
return Settings.getInstance().getDefaultArchiveVersion();
|
||||
}
|
||||
|
||||
public static int getMaxArchiveHeight(Repository repository) throws DataException {
|
||||
@@ -72,8 +129,7 @@ public class BlockArchiveWriter {
|
||||
|
||||
public BlockArchiveWriteResult write() throws DataException, IOException, TransformationException, InterruptedException {
|
||||
// Create the archive folder if it doesn't exist
|
||||
// This is a subfolder of the db directory, to make bootstrapping easier
|
||||
Path archivePath = Paths.get(Settings.getInstance().getRepositoryPath(), "archive").toAbsolutePath();
|
||||
// This is generally a subfolder of the db directory, to make bootstrapping easier
|
||||
try {
|
||||
Files.createDirectories(archivePath);
|
||||
} catch (IOException e) {
|
||||
@@ -95,13 +151,13 @@ public class BlockArchiveWriter {
|
||||
|
||||
LOGGER.info(String.format("Fetching blocks from height %d...", startHeight));
|
||||
int i = 0;
|
||||
while (headerBytes.size() + bytes.size() < this.fileSizeTarget
|
||||
|| this.shouldEnforceFileSizeTarget == false) {
|
||||
while (headerBytes.size() + bytes.size() < this.fileSizeTarget) {
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return BlockArchiveWriteResult.STOPPING;
|
||||
}
|
||||
if (Synchronizer.getInstance().isSynchronizing()) {
|
||||
Thread.sleep(1000L);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -112,7 +168,28 @@ public class BlockArchiveWriter {
|
||||
|
||||
//LOGGER.info("Fetching block {}...", currentHeight);
|
||||
|
||||
BlockData blockData = repository.getBlockRepository().fromHeight(currentHeight);
|
||||
BlockData blockData = null;
|
||||
List<TransactionData> transactions = null;
|
||||
List<ATStateData> atStates = null;
|
||||
byte[] atStatesHash = null;
|
||||
|
||||
switch (this.dataSource) {
|
||||
case BLOCK_ARCHIVE:
|
||||
BlockTransformation archivedBlock = BlockArchiveReader.getInstance().fetchBlockAtHeight(currentHeight);
|
||||
if (archivedBlock != null) {
|
||||
blockData = archivedBlock.getBlockData();
|
||||
transactions = archivedBlock.getTransactions();
|
||||
atStates = archivedBlock.getAtStates();
|
||||
atStatesHash = archivedBlock.getAtStatesHash();
|
||||
}
|
||||
break;
|
||||
|
||||
case BLOCK_REPOSITORY:
|
||||
default:
|
||||
blockData = repository.getBlockRepository().fromHeight(currentHeight);
|
||||
break;
|
||||
}
|
||||
|
||||
if (blockData == null) {
|
||||
return BlockArchiveWriteResult.BLOCK_NOT_FOUND;
|
||||
}
|
||||
@@ -122,18 +199,50 @@ public class BlockArchiveWriter {
|
||||
repository.getBlockArchiveRepository().save(blockArchiveData);
|
||||
repository.saveChanges();
|
||||
|
||||
// Build the block
|
||||
Block block;
|
||||
if (atStatesHash != null) {
|
||||
block = new Block(repository, blockData, transactions, atStatesHash);
|
||||
}
|
||||
else if (atStates != null) {
|
||||
block = new Block(repository, blockData, transactions, atStates);
|
||||
}
|
||||
else {
|
||||
block = new Block(repository, blockData);
|
||||
}
|
||||
|
||||
// Write the block data to some byte buffers
|
||||
Block block = new Block(repository, blockData);
|
||||
int blockIndex = bytes.size();
|
||||
// Write block index to header
|
||||
headerBytes.write(Ints.toByteArray(blockIndex));
|
||||
// Write block height
|
||||
bytes.write(Ints.toByteArray(block.getBlockData().getHeight()));
|
||||
byte[] blockBytes = BlockTransformer.toBytes(block);
|
||||
|
||||
// Get serialized block bytes
|
||||
byte[] blockBytes;
|
||||
switch (serializationVersion) {
|
||||
case 1:
|
||||
blockBytes = BlockTransformer.toBytes(block);
|
||||
break;
|
||||
|
||||
case 2:
|
||||
blockBytes = BlockTransformer.toBytesV2(block);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new DataException("Invalid serialization version");
|
||||
}
|
||||
|
||||
// Write block length
|
||||
bytes.write(Ints.toByteArray(blockBytes.length));
|
||||
// Write block bytes
|
||||
bytes.write(blockBytes);
|
||||
|
||||
// Log every 1000 blocks
|
||||
if (this.shouldLogProgress && i % 1000 == 0) {
|
||||
LOGGER.info("Archived up to block height {}. Size of current file: {} bytes", currentHeight, (headerBytes.size() + bytes.size()));
|
||||
}
|
||||
|
||||
i++;
|
||||
|
||||
}
|
||||
@@ -147,11 +256,10 @@ public class BlockArchiveWriter {
|
||||
|
||||
// We have enough blocks to create a new file
|
||||
int endHeight = startHeight + i - 1;
|
||||
int version = 1;
|
||||
String filePath = String.format("%s/%d-%d.dat", archivePath.toString(), startHeight, endHeight);
|
||||
FileOutputStream fileOutputStream = new FileOutputStream(filePath);
|
||||
// Write version number
|
||||
fileOutputStream.write(Ints.toByteArray(version));
|
||||
fileOutputStream.write(Ints.toByteArray(serializationVersion));
|
||||
// Write start height
|
||||
fileOutputStream.write(Ints.toByteArray(startHeight));
|
||||
// Write end height
|
||||
@@ -199,4 +307,12 @@ public class BlockArchiveWriter {
|
||||
this.shouldEnforceFileSizeTarget = shouldEnforceFileSizeTarget;
|
||||
}
|
||||
|
||||
public void setDataSource(BlockArchiveDataSource dataSource) {
|
||||
this.dataSource = dataSource;
|
||||
}
|
||||
|
||||
public void setShouldLogProgress(boolean shouldLogProgress) {
|
||||
this.shouldLogProgress = shouldLogProgress;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -279,7 +279,9 @@ public class Bootstrap {
|
||||
|
||||
LOGGER.info("Generating checksum file...");
|
||||
String checksum = Crypto.digestHexString(compressedOutputPath.toFile(), 1024*1024);
|
||||
LOGGER.info("checksum: {}", checksum);
|
||||
Path checksumPath = Paths.get(String.format("%s.sha256", compressedOutputPath.toString()));
|
||||
LOGGER.info("Writing checksum to path: {}", checksumPath);
|
||||
Files.writeString(checksumPath, checksum, StandardOpenOption.CREATE);
|
||||
|
||||
// Return the path to the compressed bootstrap file
|
||||
|
@@ -6,6 +6,8 @@ import org.qortal.data.chat.ActiveChats;
|
||||
import org.qortal.data.chat.ChatMessage;
|
||||
import org.qortal.data.transaction.ChatTransactionData;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
public interface ChatRepository {
|
||||
|
||||
/**
|
||||
@@ -15,10 +17,11 @@ public interface ChatRepository {
|
||||
*/
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after,
|
||||
Integer txGroupId, byte[] reference, byte[] chatReferenceBytes, Boolean hasChatReference,
|
||||
List<String> involving, Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
List<String> involving, String senderAddress, Encoding encoding,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException;
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException;
|
||||
|
||||
public ActiveChats getActiveChats(String address) throws DataException;
|
||||
public ActiveChats getActiveChats(String address, Encoding encoding) throws DataException;
|
||||
|
||||
}
|
||||
|
@@ -2,11 +2,6 @@ package org.qortal.repository;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabaseArchiving;
|
||||
import org.qortal.repository.hsqldb.HSQLDBDatabasePruning;
|
||||
import org.qortal.repository.hsqldb.HSQLDBRepository;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -61,62 +56,6 @@ public abstract class RepositoryManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean archive(Repository repository) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes have no blockchain
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bulk archive the database the first time we use archive mode
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
if (RepositoryManager.canArchiveOrPrune()) {
|
||||
try {
|
||||
return HSQLDBDatabaseArchiving.buildBlockArchive(repository, BlockArchiveWriter.DEFAULT_FILE_SIZE_TARGET);
|
||||
|
||||
} catch (DataException e) {
|
||||
LOGGER.info("Unable to build block archive. The database may have been left in an inconsistent state.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.info("Unable to build block archive due to missing ATStatesHeightIndex. Bootstrapping is recommended.");
|
||||
LOGGER.info("To bootstrap, stop the core and delete the db folder, then start the core again.");
|
||||
SplashFrame.getInstance().updateStatus("Missing index. Bootstrapping is recommended.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean prune(Repository repository) {
|
||||
if (Settings.getInstance().isLite()) {
|
||||
// Lite nodes have no blockchain
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bulk prune the database the first time we use top-only or block archive mode
|
||||
if (Settings.getInstance().isTopOnly() ||
|
||||
Settings.getInstance().isArchiveEnabled()) {
|
||||
if (RepositoryManager.canArchiveOrPrune()) {
|
||||
try {
|
||||
boolean prunedATStates = HSQLDBDatabasePruning.pruneATStates((HSQLDBRepository) repository);
|
||||
boolean prunedBlocks = HSQLDBDatabasePruning.pruneBlocks((HSQLDBRepository) repository);
|
||||
|
||||
// Perform repository maintenance to shrink the db size down
|
||||
if (prunedATStates && prunedBlocks) {
|
||||
HSQLDBDatabasePruning.performMaintenance(repository);
|
||||
return true;
|
||||
}
|
||||
|
||||
} catch (SQLException | DataException e) {
|
||||
LOGGER.info("Unable to bulk prune AT states. The database may have been left in an inconsistent state.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.info("Unable to prune blocks due to missing ATStatesHeightIndex. Bootstrapping is recommended.");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void setRequestedCheckpoint(Boolean quick) {
|
||||
quickCheckpointRequested = quick;
|
||||
}
|
||||
|
@@ -9,6 +9,8 @@ public interface VotingRepository {
|
||||
|
||||
// Polls
|
||||
|
||||
public List<PollData> getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||
|
||||
public PollData fromPollName(String pollName) throws DataException;
|
||||
|
||||
public boolean pollExists(String pollName) throws DataException;
|
||||
|
@@ -603,7 +603,7 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
|
||||
|
||||
@Override
|
||||
public void rebuildLatestAtStates() throws DataException {
|
||||
public void rebuildLatestAtStates(int maxHeight) throws DataException {
|
||||
// latestATStatesLock is to prevent concurrent updates on LatestATStates
|
||||
// that could result in one process using a partial or empty dataset
|
||||
// because it was in the process of being rebuilt by another thread
|
||||
@@ -624,11 +624,12 @@ public class HSQLDBATRepository implements ATRepository {
|
||||
+ "CROSS JOIN LATERAL("
|
||||
+ "SELECT height FROM ATStates "
|
||||
+ "WHERE ATStates.AT_address = ATs.AT_address "
|
||||
+ "AND height <= ?"
|
||||
+ "ORDER BY AT_address DESC, height DESC LIMIT 1"
|
||||
+ ") "
|
||||
+ ")";
|
||||
try {
|
||||
this.repository.executeCheckedUpdate(insertSql);
|
||||
this.repository.executeCheckedUpdate(insertSql, maxHeight);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to populate temporary latest AT states cache in repository", e);
|
||||
|
@@ -5,9 +5,7 @@ import org.apache.logging.log4j.Logger;
|
||||
import org.bouncycastle.util.Longs;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceNameInfo;
|
||||
import org.qortal.data.network.ArbitraryPeerData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
@@ -15,8 +13,10 @@ import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.repository.ArbitraryRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.transaction.ArbitraryTransaction;
|
||||
import org.qortal.transaction.Transaction.ApprovalStatus;
|
||||
import org.qortal.utils.Base58;
|
||||
import org.qortal.utils.ListUtils;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
@@ -27,8 +27,6 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBArbitraryRepository.class);
|
||||
|
||||
private static final int MAX_RAW_DATA_SIZE = 255; // size of VARBINARY
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
|
||||
public HSQLDBArbitraryRepository(HSQLDBRepository repository) {
|
||||
@@ -55,13 +53,8 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Load hashes
|
||||
byte[] hash = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
|
||||
// Load data file(s)
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
// Check if we already have the complete data file or all chunks
|
||||
if (arbitraryDataFile.allFilesExist()) {
|
||||
@@ -84,13 +77,8 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
return transactionData.getData();
|
||||
}
|
||||
|
||||
// Load hashes
|
||||
byte[] digest = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
|
||||
// Load data file(s)
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
// If we have the complete data file, return it
|
||||
if (arbitraryDataFile.exists()) {
|
||||
@@ -105,6 +93,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
arbitraryDataFile.join();
|
||||
|
||||
// Verify that the combined hash matches the expected hash
|
||||
byte[] digest = transactionData.getData();
|
||||
if (!digest.equals(arbitraryDataFile.digest())) {
|
||||
LOGGER.info(String.format("Hash mismatch for transaction: %s", Base58.encode(signature)));
|
||||
return null;
|
||||
@@ -132,11 +121,11 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
}
|
||||
|
||||
// Trivial-sized payloads can remain in raw form
|
||||
if (arbitraryTransactionData.getDataType() == DataType.RAW_DATA && arbitraryTransactionData.getData().length <= MAX_RAW_DATA_SIZE) {
|
||||
if (arbitraryTransactionData.getDataType() == DataType.RAW_DATA && arbitraryTransactionData.getData().length <= ArbitraryTransaction.MAX_DATA_SIZE) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new IllegalStateException(String.format("Supplied data is larger than maximum size (%d bytes). Please use ArbitraryDataWriter.", MAX_RAW_DATA_SIZE));
|
||||
throw new IllegalStateException(String.format("Supplied data is larger than maximum size (%d bytes). Please use ArbitraryDataWriter.", ArbitraryTransaction.MAX_DATA_SIZE));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -146,17 +135,11 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load hashes
|
||||
byte[] hash = arbitraryTransactionData.getData();
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
|
||||
// Load data file(s)
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(hash, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
|
||||
// Delete file and chunks
|
||||
arbitraryDataFile.deleteAll();
|
||||
// Delete file, chunks, and metadata
|
||||
arbitraryDataFile.deleteAll(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -202,7 +185,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
int version = resultSet.getInt(11);
|
||||
int nonce = resultSet.getInt(12);
|
||||
Service serviceResult = Service.valueOf(resultSet.getInt(13));
|
||||
int serviceInt = resultSet.getInt(13);
|
||||
int size = resultSet.getInt(14);
|
||||
boolean isDataRaw = resultSet.getBoolean(15); // NOT NULL, so no null to false
|
||||
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
|
||||
@@ -216,7 +199,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
// FUTURE: get payments from signature if needed. Avoiding for now to reduce database calls.
|
||||
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, serviceResult, nonce, size, nameResult, identifierResult, method, secret,
|
||||
version, serviceInt, nonce, size, nameResult, identifierResult, method, secret,
|
||||
compression, data, dataType, metadataHash, null);
|
||||
|
||||
arbitraryTransactionData.add(transactionData);
|
||||
@@ -277,7 +260,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
int version = resultSet.getInt(11);
|
||||
int nonce = resultSet.getInt(12);
|
||||
Service serviceResult = Service.valueOf(resultSet.getInt(13));
|
||||
int serviceInt = resultSet.getInt(13);
|
||||
int size = resultSet.getInt(14);
|
||||
boolean isDataRaw = resultSet.getBoolean(15); // NOT NULL, so no null to false
|
||||
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
|
||||
@@ -291,7 +274,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
// FUTURE: get payments from signature if needed. Avoiding for now to reduce database calls.
|
||||
|
||||
ArbitraryTransactionData transactionData = new ArbitraryTransactionData(baseTransactionData,
|
||||
version, serviceResult, nonce, size, nameResult, identifierResult, methodResult, secret,
|
||||
version, serviceInt, nonce, size, nameResult, identifierResult, methodResult, secret,
|
||||
compression, data, dataType, metadataHash, null);
|
||||
|
||||
return transactionData;
|
||||
@@ -302,7 +285,8 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
|
||||
@Override
|
||||
public List<ArbitraryResourceInfo> getArbitraryResources(Service service, String identifier, List<String> names,
|
||||
boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
@@ -337,6 +321,36 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
sql.append(")");
|
||||
}
|
||||
|
||||
// Handle "followed only"
|
||||
if (followedOnly != null && followedOnly) {
|
||||
List<String> followedNames = ListUtils.followedNames();
|
||||
if (followedNames != null && !followedNames.isEmpty()) {
|
||||
sql.append(" AND name IN (?");
|
||||
bindParams.add(followedNames.get(0));
|
||||
|
||||
for (int i = 1; i < followedNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(followedNames.get(i));
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle "exclude blocked"
|
||||
if (excludeBlocked != null && excludeBlocked) {
|
||||
List<String> blockedNames = ListUtils.blockedNames();
|
||||
if (blockedNames != null && !blockedNames.isEmpty()) {
|
||||
sql.append(" AND name NOT IN (?");
|
||||
bindParams.add(blockedNames.get(0));
|
||||
|
||||
for (int i = 1; i < blockedNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(blockedNames.get(i));
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
}
|
||||
|
||||
sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||
|
||||
if (reverse != null && reverse) {
|
||||
@@ -378,37 +392,107 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query,
|
||||
boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
public List<ArbitraryResourceInfo> searchArbitraryResources(Service service, String query, String identifier, List<String> names, boolean prefixOnly,
|
||||
List<String> exactMatchNames, boolean defaultResource, Boolean followedOnly, Boolean excludeBlocked,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
List<Object> bindParams = new ArrayList<>();
|
||||
|
||||
// For now we are searching anywhere in the fields
|
||||
// Note that this will bypass any indexes so may not scale well
|
||||
// Longer term we probably want to copy resources to their own table anyway
|
||||
String queryWildcard = String.format("%%%s%%", query.toLowerCase());
|
||||
|
||||
sql.append("SELECT name, service, identifier, MAX(size) AS max_size FROM ArbitraryTransactions WHERE 1=1");
|
||||
sql.append("SELECT name, service, identifier, MAX(size) AS max_size, MIN(created_when) AS date_created, MAX(created_when) AS date_updated " +
|
||||
"FROM ArbitraryTransactions " +
|
||||
"JOIN Transactions USING (signature) " +
|
||||
"WHERE 1=1");
|
||||
|
||||
if (service != null) {
|
||||
sql.append(" AND service = ");
|
||||
sql.append(service.value);
|
||||
}
|
||||
|
||||
if (defaultResource) {
|
||||
// Default resource requested - use NULL identifier and search name only
|
||||
sql.append(" AND LCASE(name) LIKE ? AND identifier IS NULL");
|
||||
bindParams.add(queryWildcard);
|
||||
// Handle general query matches
|
||||
if (query != null) {
|
||||
// Search anywhere in the fields, unless "prefixOnly" has been requested
|
||||
// Note that without prefixOnly it will bypass any indexes so may not scale well
|
||||
// Longer term we probably want to copy resources to their own table anyway
|
||||
String queryWildcard = prefixOnly ? String.format("%s%%", query.toLowerCase()) : String.format("%%%s%%", query.toLowerCase());
|
||||
|
||||
if (defaultResource) {
|
||||
// Default resource requested - use NULL identifier and search name only
|
||||
sql.append(" AND LCASE(name) LIKE ? AND identifier IS NULL");
|
||||
bindParams.add(queryWildcard);
|
||||
} else {
|
||||
// Non-default resource requested
|
||||
// In this case we search the identifier as well as the name
|
||||
sql.append(" AND (LCASE(name) LIKE ? OR LCASE(identifier) LIKE ?)");
|
||||
bindParams.add(queryWildcard);
|
||||
bindParams.add(queryWildcard);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Non-default resource requested
|
||||
// In this case we search the identifier as well as the name
|
||||
sql.append(" AND (LCASE(name) LIKE ? OR LCASE(identifier) LIKE ?)");
|
||||
bindParams.add(queryWildcard);
|
||||
|
||||
// Handle identifier matches
|
||||
if (identifier != null) {
|
||||
// Search anywhere in the identifier, unless "prefixOnly" has been requested
|
||||
String queryWildcard = prefixOnly ? String.format("%s%%", identifier.toLowerCase()) : String.format("%%%s%%", identifier.toLowerCase());
|
||||
sql.append(" AND LCASE(identifier) LIKE ?");
|
||||
bindParams.add(queryWildcard);
|
||||
}
|
||||
|
||||
sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD");
|
||||
// Handle name searches
|
||||
if (names != null && !names.isEmpty()) {
|
||||
sql.append(" AND (");
|
||||
|
||||
for (int i = 0; i < names.size(); ++i) {
|
||||
// Search anywhere in the name, unless "prefixOnly" has been requested
|
||||
String queryWildcard = prefixOnly ? String.format("%s%%", names.get(i).toLowerCase()) : String.format("%%%s%%", names.get(i).toLowerCase());
|
||||
if (i > 0) sql.append(" OR ");
|
||||
sql.append("LCASE(name) LIKE ?");
|
||||
bindParams.add(queryWildcard);
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
|
||||
// Handle name exact matches
|
||||
if (exactMatchNames != null && !exactMatchNames.isEmpty()) {
|
||||
sql.append(" AND name IN (?");
|
||||
bindParams.add(exactMatchNames.get(0));
|
||||
|
||||
for (int i = 1; i < exactMatchNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(exactMatchNames.get(i));
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
|
||||
// Handle "followed only"
|
||||
if (followedOnly != null && followedOnly) {
|
||||
List<String> followedNames = ListUtils.followedNames();
|
||||
if (followedNames != null && !followedNames.isEmpty()) {
|
||||
sql.append(" AND name IN (?");
|
||||
bindParams.add(followedNames.get(0));
|
||||
|
||||
for (int i = 1; i < followedNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(followedNames.get(i));
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
}
|
||||
|
||||
// Handle "exclude blocked"
|
||||
if (excludeBlocked != null && excludeBlocked) {
|
||||
List<String> blockedNames = ListUtils.blockedNames();
|
||||
if (blockedNames != null && !blockedNames.isEmpty()) {
|
||||
sql.append(" AND name NOT IN (?");
|
||||
bindParams.add(blockedNames.get(0));
|
||||
|
||||
for (int i = 1; i < blockedNames.size(); ++i) {
|
||||
sql.append(", ?");
|
||||
bindParams.add(blockedNames.get(i));
|
||||
}
|
||||
sql.append(")");
|
||||
}
|
||||
}
|
||||
|
||||
sql.append(" GROUP BY name, service, identifier ORDER BY date_created");
|
||||
|
||||
if (reverse != null && reverse) {
|
||||
sql.append(" DESC");
|
||||
@@ -427,6 +511,8 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
Service serviceResult = Service.valueOf(resultSet.getInt(2));
|
||||
String identifierResult = resultSet.getString(3);
|
||||
Integer sizeResult = resultSet.getInt(4);
|
||||
long dateCreated = resultSet.getLong(5);
|
||||
long dateUpdated = resultSet.getLong(6);
|
||||
|
||||
// We should filter out resources without names
|
||||
if (nameResult == null) {
|
||||
@@ -438,6 +524,8 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository {
|
||||
arbitraryResourceInfo.service = serviceResult;
|
||||
arbitraryResourceInfo.identifier = identifierResult;
|
||||
arbitraryResourceInfo.size = Longs.valueOf(sizeResult);
|
||||
arbitraryResourceInfo.created = dateCreated;
|
||||
arbitraryResourceInfo.updated = dateUpdated;
|
||||
|
||||
arbitraryResources.add(arbitraryResourceInfo);
|
||||
} while (resultSet.next());
|
||||
|
@@ -14,6 +14,8 @@ import org.qortal.repository.ChatRepository;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
|
||||
import static org.qortal.data.chat.ChatMessage.Encoding;
|
||||
|
||||
public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
protected HSQLDBRepository repository;
|
||||
@@ -24,8 +26,8 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
@Override
|
||||
public List<ChatMessage> getMessagesMatchingCriteria(Long before, Long after, Integer txGroupId, byte[] referenceBytes,
|
||||
byte[] chatReferenceBytes, Boolean hasChatReference, List<String> involving,
|
||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
byte[] chatReferenceBytes, Boolean hasChatReference, List<String> involving, String senderAddress,
|
||||
Encoding encoding, Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
// Check args meet expectations
|
||||
if ((txGroupId != null && involving != null && !involving.isEmpty())
|
||||
|| (txGroupId == null && (involving == null || involving.size() != 2)))
|
||||
@@ -74,6 +76,11 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
whereClauses.add("chat_reference IS NULL");
|
||||
}
|
||||
|
||||
if (senderAddress != null) {
|
||||
whereClauses.add("sender = ?");
|
||||
bindParams.add(senderAddress);
|
||||
}
|
||||
|
||||
if (txGroupId != null) {
|
||||
whereClauses.add("tx_group_id = " + txGroupId); // int safe to use literally
|
||||
whereClauses.add("recipient IS NULL");
|
||||
@@ -122,7 +129,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
byte[] signature = resultSet.getBytes(13);
|
||||
|
||||
ChatMessage chatMessage = new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||
senderName, recipient, recipientName, chatReference, encoding, data, isText, isEncrypted, signature);
|
||||
|
||||
chatMessages.add(chatMessage);
|
||||
} while (resultSet.next());
|
||||
@@ -134,7 +141,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData) throws DataException {
|
||||
public ChatMessage toChatMessage(ChatTransactionData chatTransactionData, Encoding encoding) throws DataException {
|
||||
String sql = "SELECT SenderNames.name, RecipientNames.name "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -161,27 +168,28 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
byte[] signature = chatTransactionData.getSignature();
|
||||
|
||||
return new ChatMessage(timestamp, groupId, reference, senderPublicKey, sender,
|
||||
senderName, recipient, recipientName, chatReference, data, isText, isEncrypted, signature);
|
||||
senderName, recipient, recipientName, chatReference, encoding, data,
|
||||
isText, isEncrypted, signature);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch convert chat transaction from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActiveChats getActiveChats(String address) throws DataException {
|
||||
List<GroupChat> groupChats = getActiveGroupChats(address);
|
||||
public ActiveChats getActiveChats(String address, Encoding encoding) throws DataException {
|
||||
List<GroupChat> groupChats = getActiveGroupChats(address, encoding);
|
||||
List<DirectChat> directChats = getActiveDirectChats(address);
|
||||
|
||||
return new ActiveChats(groupChats, directChats);
|
||||
}
|
||||
|
||||
private List<GroupChat> getActiveGroupChats(String address) throws DataException {
|
||||
private List<GroupChat> getActiveGroupChats(String address, Encoding encoding) throws DataException {
|
||||
// Find groups where address is a member and potential latest message details
|
||||
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name "
|
||||
String groupsSql = "SELECT group_id, group_name, latest_timestamp, sender, sender_name, signature, data "
|
||||
+ "FROM GroupMembers "
|
||||
+ "JOIN Groups USING (group_id) "
|
||||
+ "LEFT OUTER JOIN LATERAL("
|
||||
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name "
|
||||
+ "SELECT created_when AS latest_timestamp, sender, name AS sender_name, signature, data "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -205,8 +213,10 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
|
||||
String sender = resultSet.getString(4);
|
||||
String senderName = resultSet.getString(5);
|
||||
byte[] signature = resultSet.getBytes(6);
|
||||
byte[] data = resultSet.getBytes(7);
|
||||
|
||||
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName);
|
||||
GroupChat groupChat = new GroupChat(groupId, groupName, timestamp, sender, senderName, signature, encoding, data);
|
||||
groupChats.add(groupChat);
|
||||
} while (resultSet.next());
|
||||
}
|
||||
@@ -215,7 +225,7 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
}
|
||||
|
||||
// We need different SQL to handle group-less chat
|
||||
String grouplessSql = "SELECT created_when, sender, SenderNames.name "
|
||||
String grouplessSql = "SELECT created_when, sender, SenderNames.name, signature, data "
|
||||
+ "FROM ChatTransactions "
|
||||
+ "JOIN Transactions USING (signature) "
|
||||
+ "LEFT OUTER JOIN Names AS SenderNames ON SenderNames.owner = sender "
|
||||
@@ -228,15 +238,19 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
Long timestamp = null;
|
||||
String sender = null;
|
||||
String senderName = null;
|
||||
byte[] signature = null;
|
||||
byte[] data = null;
|
||||
|
||||
if (resultSet != null) {
|
||||
// We found a recipient-less, group-less CHAT message, so report its details
|
||||
timestamp = resultSet.getLong(1);
|
||||
sender = resultSet.getString(2);
|
||||
senderName = resultSet.getString(3);
|
||||
signature = resultSet.getBytes(4);
|
||||
data = resultSet.getBytes(5);
|
||||
}
|
||||
|
||||
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName);
|
||||
GroupChat groupChat = new GroupChat(0, null, timestamp, sender, senderName, signature, encoding, data);
|
||||
groupChats.add(groupChat);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch active group chats from repository", e);
|
||||
@@ -291,4 +305,4 @@ public class HSQLDBChatRepository implements ChatRepository {
|
||||
return directChats;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.repository.BlockArchiveWriter;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
import org.qortal.transform.TransformationException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
*
|
||||
* When switching to an archiving node, we need to archive most of the database contents.
|
||||
* This involves copying its data into flat files.
|
||||
* If we do this entirely as a background process, it is very slow and can interfere with syncing.
|
||||
* However, if we take the approach of doing this in bulk, before starting up the rest of the
|
||||
* processes, this makes it much faster and less invasive.
|
||||
*
|
||||
* From that point, the original background archiving process will run, but can be dialled right down
|
||||
* so not to interfere with syncing.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
public class HSQLDBDatabaseArchiving {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabaseArchiving.class);
|
||||
|
||||
|
||||
public static boolean buildBlockArchive(Repository repository, long fileSizeTarget) throws DataException {
|
||||
|
||||
// Only build the archive if we haven't already got one that is up to date
|
||||
boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
|
||||
if (upToDate) {
|
||||
// Already archived
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGGER.info("Building block archive - this process could take a while...");
|
||||
SplashFrame.getInstance().updateStatus("Building block archive...");
|
||||
|
||||
final int maximumArchiveHeight = BlockArchiveWriter.getMaxArchiveHeight(repository);
|
||||
int startHeight = 0;
|
||||
|
||||
while (!Controller.isStopping()) {
|
||||
try {
|
||||
BlockArchiveWriter writer = new BlockArchiveWriter(startHeight, maximumArchiveHeight, repository);
|
||||
writer.setFileSizeTarget(fileSizeTarget);
|
||||
BlockArchiveWriter.BlockArchiveWriteResult result = writer.write();
|
||||
switch (result) {
|
||||
case OK:
|
||||
// Increment block archive height
|
||||
startHeight = writer.getLastWrittenHeight() + 1;
|
||||
repository.getBlockArchiveRepository().setBlockArchiveHeight(startHeight);
|
||||
repository.saveChanges();
|
||||
break;
|
||||
|
||||
case STOPPING:
|
||||
return false;
|
||||
|
||||
case NOT_ENOUGH_BLOCKS:
|
||||
// We've reached the limit of the blocks we can archive
|
||||
// Return from the whole method
|
||||
return true;
|
||||
|
||||
case BLOCK_NOT_FOUND:
|
||||
// We tried to archive a block that didn't exist. This is a major failure and likely means
|
||||
// that a bootstrap or re-sync is needed. Return rom the method
|
||||
LOGGER.info("Error: block not found when building archive. If this error persists, " +
|
||||
"a bootstrap or re-sync may be needed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
} catch (IOException | TransformationException | InterruptedException e) {
|
||||
LOGGER.info("Caught exception when creating block cache", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we got this far then something went wrong (most likely the app is stopping)
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
@@ -1,332 +0,0 @@
|
||||
package org.qortal.repository.hsqldb;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.data.block.BlockData;
|
||||
import org.qortal.gui.SplashFrame;
|
||||
import org.qortal.repository.BlockArchiveWriter;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
/**
|
||||
*
|
||||
* When switching from a full node to a pruning node, we need to delete most of the database contents.
|
||||
* If we do this entirely as a background process, it is very slow and can interfere with syncing.
|
||||
* However, if we take the approach of transferring only the necessary rows to a new table and then
|
||||
* deleting the original table, this makes the process much faster. It was taking several days to
|
||||
* delete the AT states in the background, but only a couple of minutes to copy them to a new table.
|
||||
*
|
||||
* The trade off is that we have to go through a form of "reshape" when starting the app for the first
|
||||
* time after enabling pruning mode. But given that this is an opt-in mode, I don't think it will be
|
||||
* a problem.
|
||||
*
|
||||
* Once the pruning is complete, it automatically performs a CHECKPOINT DEFRAG in order to
|
||||
* shrink the database file size down to a fraction of what it was before.
|
||||
*
|
||||
* From this point, the original background process will run, but can be dialled right down so not
|
||||
* to interfere with syncing.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
public class HSQLDBDatabasePruning {
|
||||
|
||||
private static final Logger LOGGER = LogManager.getLogger(HSQLDBDatabasePruning.class);
|
||||
|
||||
|
||||
public static boolean pruneATStates(HSQLDBRepository repository) throws SQLException, DataException {
|
||||
|
||||
// Only bulk prune AT states if we have never done so before
|
||||
int pruneHeight = repository.getATRepository().getAtPruneHeight();
|
||||
if (pruneHeight > 0) {
|
||||
// Already pruned AT states
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Only proceed if we can see that the archiver has already finished
|
||||
// This way, if the archiver failed for any reason, we can prune once it has had
|
||||
// some opportunities to try again
|
||||
boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
|
||||
if (!upToDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("Starting bulk prune of AT states - this process could take a while... " +
|
||||
"(approx. 2 mins on high spec, or upwards of 30 mins in some cases)");
|
||||
SplashFrame.getInstance().updateStatus("Pruning database (takes up to 30 mins)...");
|
||||
|
||||
// Create new AT-states table to hold smaller dataset
|
||||
repository.executeCheckedUpdate("DROP TABLE IF EXISTS ATStatesNew");
|
||||
repository.executeCheckedUpdate("CREATE TABLE ATStatesNew ("
|
||||
+ "AT_address QortalAddress, height INTEGER NOT NULL, state_hash ATStateHash NOT NULL, "
|
||||
+ "fees QortalAmount NOT NULL, is_initial BOOLEAN NOT NULL, sleep_until_message_timestamp BIGINT, "
|
||||
+ "PRIMARY KEY (AT_address, height), "
|
||||
+ "FOREIGN KEY (AT_address) REFERENCES ATs (AT_address) ON DELETE CASCADE)");
|
||||
repository.executeCheckedUpdate("SET TABLE ATStatesNew NEW SPACE");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Add a height index
|
||||
LOGGER.info("Adding index to AT states table...");
|
||||
repository.executeCheckedUpdate("CREATE INDEX IF NOT EXISTS ATStatesNewHeightIndex ON ATStatesNew (height)");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
|
||||
// Find our latest block
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
if (latestBlock == null) {
|
||||
LOGGER.info("Unable to determine blockchain height, necessary for bulk block pruning");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate some constants for later use
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
int maximumBlockToTrim = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Archive mode - don't prune anything that hasn't been archived yet
|
||||
maximumBlockToTrim = Math.min(maximumBlockToTrim, repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1);
|
||||
}
|
||||
final int endHeight = blockchainHeight;
|
||||
final int blockStep = 10000;
|
||||
|
||||
|
||||
// It's essential that we rebuild the latest AT states here, as we are using this data in the next query.
|
||||
// Failing to do this will result in important AT states being deleted, rendering the database unusable.
|
||||
repository.getATRepository().rebuildLatestAtStates();
|
||||
|
||||
|
||||
// Loop through all the LatestATStates and copy them to the new table
|
||||
LOGGER.info("Copying AT states...");
|
||||
for (int height = 0; height < endHeight; height += blockStep) {
|
||||
final int batchEndHeight = height + blockStep - 1;
|
||||
//LOGGER.info(String.format("Copying AT states between %d and %d...", height, batchEndHeight));
|
||||
|
||||
String sql = "SELECT height, AT_address FROM LatestATStates WHERE height BETWEEN ? AND ?";
|
||||
try (ResultSet latestAtStatesResultSet = repository.checkedExecute(sql, height, batchEndHeight)) {
|
||||
if (latestAtStatesResultSet != null) {
|
||||
do {
|
||||
int latestAtHeight = latestAtStatesResultSet.getInt(1);
|
||||
String latestAtAddress = latestAtStatesResultSet.getString(2);
|
||||
|
||||
// Copy this latest ATState to the new table
|
||||
//LOGGER.info(String.format("Copying AT %s at height %d...", latestAtAddress, latestAtHeight));
|
||||
try {
|
||||
String updateSql = "INSERT INTO ATStatesNew ("
|
||||
+ "SELECT AT_address, height, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height = ? AND AT_address = ?)";
|
||||
repository.executeCheckedUpdate(updateSql, latestAtHeight, latestAtAddress);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to copy ATStates", e);
|
||||
}
|
||||
|
||||
// If this batch includes blocks after the maximum block to trim, we will need to copy
|
||||
// each of its AT states above maximumBlockToTrim as they are considered "recent". We
|
||||
// need to do this for _all_ AT states in these blocks, regardless of their latest state.
|
||||
if (batchEndHeight >= maximumBlockToTrim) {
|
||||
// Now copy this AT's states for each recent block they are present in
|
||||
for (int i = maximumBlockToTrim; i < endHeight; i++) {
|
||||
if (latestAtHeight < i) {
|
||||
// This AT finished before this block so there is nothing to copy
|
||||
continue;
|
||||
}
|
||||
|
||||
//LOGGER.info(String.format("Copying recent AT %s at height %d...", latestAtAddress, i));
|
||||
try {
|
||||
// Copy each LatestATState to the new table
|
||||
String updateSql = "INSERT IGNORE INTO ATStatesNew ("
|
||||
+ "SELECT AT_address, height, state_hash, fees, is_initial, sleep_until_message_timestamp "
|
||||
+ "FROM ATStates "
|
||||
+ "WHERE height = ? AND AT_address = ?)";
|
||||
repository.executeCheckedUpdate(updateSql, i, latestAtAddress);
|
||||
} catch (SQLException e) {
|
||||
repository.examineException(e);
|
||||
throw new DataException("Unable to copy ATStates", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
repository.saveChanges();
|
||||
|
||||
} while (latestAtStatesResultSet.next());
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to copy AT states", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Finally, drop the original table and rename
|
||||
LOGGER.info("Deleting old AT states...");
|
||||
repository.executeCheckedUpdate("DROP TABLE ATStates");
|
||||
repository.executeCheckedUpdate("ALTER TABLE ATStatesNew RENAME TO ATStates");
|
||||
repository.executeCheckedUpdate("ALTER INDEX ATStatesNewHeightIndex RENAME TO ATStatesHeightIndex");
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Update the prune height
|
||||
int nextPruneHeight = maximumBlockToTrim + 1;
|
||||
repository.getATRepository().setAtPruneHeight(nextPruneHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
repository.executeCheckedUpdate("CHECKPOINT");
|
||||
|
||||
// Now prune/trim the ATStatesData, as this currently goes back over a month
|
||||
return HSQLDBDatabasePruning.pruneATStateData(repository);
|
||||
}
|
||||
|
||||
/*
|
||||
* Bulk prune ATStatesData to catch up with the now pruned ATStates table
|
||||
* This uses the existing AT States trimming code but with a much higher end block
|
||||
*/
|
||||
private static boolean pruneATStateData(Repository repository) throws DataException {
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Don't prune ATStatesData in archive mode
|
||||
return true;
|
||||
}
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
if (latestBlock == null) {
|
||||
LOGGER.info("Unable to determine blockchain height, necessary for bulk ATStatesData pruning");
|
||||
return false;
|
||||
}
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
int upperPrunableHeight = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
// ATStateData is already trimmed - so carry on from where we left off in the past
|
||||
int pruneStartHeight = repository.getATRepository().getAtTrimHeight();
|
||||
|
||||
LOGGER.info("Starting bulk prune of AT states data - this process could take a while... (approx. 3 mins on high spec)");
|
||||
|
||||
while (pruneStartHeight < upperPrunableHeight) {
|
||||
// Prune all AT state data up until our latest minus pruneBlockLimit (or our archive height)
|
||||
|
||||
if (Controller.isStopping()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Override batch size in the settings because this is a one-off process
|
||||
final int batchSize = 1000;
|
||||
final int rowLimitPerBatch = 50000;
|
||||
int upperBatchHeight = pruneStartHeight + batchSize;
|
||||
int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight);
|
||||
|
||||
LOGGER.trace(String.format("Pruning AT states data between %d and %d...", pruneStartHeight, upperPruneHeight));
|
||||
|
||||
int numATStatesPruned = repository.getATRepository().trimAtStates(pruneStartHeight, upperPruneHeight, rowLimitPerBatch);
|
||||
repository.saveChanges();
|
||||
|
||||
if (numATStatesPruned > 0) {
|
||||
LOGGER.trace(String.format("Pruned %d AT states data rows between blocks %d and %d",
|
||||
numATStatesPruned, pruneStartHeight, upperPruneHeight));
|
||||
} else {
|
||||
repository.getATRepository().setAtTrimHeight(upperBatchHeight);
|
||||
// No need to rebuild the latest AT states as we aren't currently synchronizing
|
||||
repository.saveChanges();
|
||||
LOGGER.debug(String.format("Bumping AT states trim height to %d", upperBatchHeight));
|
||||
|
||||
// Can we move onto next batch?
|
||||
if (upperPrunableHeight > upperBatchHeight) {
|
||||
pruneStartHeight = upperBatchHeight;
|
||||
}
|
||||
else {
|
||||
// We've finished pruning
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean pruneBlocks(Repository repository) throws SQLException, DataException {
|
||||
|
||||
// Only bulk prune AT states if we have never done so before
|
||||
int pruneHeight = repository.getBlockRepository().getBlockPruneHeight();
|
||||
if (pruneHeight > 0) {
|
||||
// Already pruned blocks
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Only proceed if we can see that the archiver has already finished
|
||||
// This way, if the archiver failed for any reason, we can prune once it has had
|
||||
// some opportunities to try again
|
||||
boolean upToDate = BlockArchiveWriter.isArchiverUpToDate(repository);
|
||||
if (!upToDate) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
BlockData latestBlock = repository.getBlockRepository().getLastBlock();
|
||||
if (latestBlock == null) {
|
||||
LOGGER.info("Unable to determine blockchain height, necessary for bulk block pruning");
|
||||
return false;
|
||||
}
|
||||
final int blockchainHeight = latestBlock.getHeight();
|
||||
int upperPrunableHeight = blockchainHeight - Settings.getInstance().getPruneBlockLimit();
|
||||
int pruneStartHeight = 0;
|
||||
|
||||
if (Settings.getInstance().isArchiveEnabled()) {
|
||||
// Archive mode - don't prune anything that hasn't been archived yet
|
||||
upperPrunableHeight = Math.min(upperPrunableHeight, repository.getBlockArchiveRepository().getBlockArchiveHeight() - 1);
|
||||
}
|
||||
|
||||
LOGGER.info("Starting bulk prune of blocks - this process could take a while... (approx. 5 mins on high spec)");
|
||||
|
||||
while (pruneStartHeight < upperPrunableHeight) {
|
||||
// Prune all blocks up until our latest minus pruneBlockLimit
|
||||
|
||||
int upperBatchHeight = pruneStartHeight + Settings.getInstance().getBlockPruneBatchSize();
|
||||
int upperPruneHeight = Math.min(upperBatchHeight, upperPrunableHeight);
|
||||
|
||||
LOGGER.info(String.format("Pruning blocks between %d and %d...", pruneStartHeight, upperPruneHeight));
|
||||
|
||||
int numBlocksPruned = repository.getBlockRepository().pruneBlocks(pruneStartHeight, upperPruneHeight);
|
||||
repository.saveChanges();
|
||||
|
||||
if (numBlocksPruned > 0) {
|
||||
LOGGER.info(String.format("Pruned %d block%s between %d and %d",
|
||||
numBlocksPruned, (numBlocksPruned != 1 ? "s" : ""),
|
||||
pruneStartHeight, upperPruneHeight));
|
||||
} else {
|
||||
final int nextPruneHeight = upperPruneHeight + 1;
|
||||
repository.getBlockRepository().setBlockPruneHeight(nextPruneHeight);
|
||||
repository.saveChanges();
|
||||
LOGGER.debug(String.format("Bumping block base prune height to %d", nextPruneHeight));
|
||||
|
||||
// Can we move onto next batch?
|
||||
if (upperPrunableHeight > nextPruneHeight) {
|
||||
pruneStartHeight = nextPruneHeight;
|
||||
}
|
||||
else {
|
||||
// We've finished pruning
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void performMaintenance(Repository repository) throws SQLException, DataException {
|
||||
try {
|
||||
SplashFrame.getInstance().updateStatus("Performing maintenance...");
|
||||
|
||||
// Timeout if the database isn't ready for backing up after 5 minutes
|
||||
// Nothing else should be using the db at this point, so a timeout shouldn't happen
|
||||
long timeout = 5 * 60 * 1000L;
|
||||
repository.performPeriodicMaintenance(timeout);
|
||||
|
||||
} catch (TimeoutException e) {
|
||||
LOGGER.info("Attempt to perform maintenance failed due to timeout: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -21,6 +21,55 @@ public class HSQLDBVotingRepository implements VotingRepository {
|
||||
|
||||
// Polls
|
||||
|
||||
@Override
|
||||
public List<PollData> getAllPolls(Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||
StringBuilder sql = new StringBuilder(512);
|
||||
|
||||
sql.append("SELECT poll_name, description, creator, owner, published_when FROM Polls ORDER BY poll_name");
|
||||
|
||||
if (reverse != null && reverse)
|
||||
sql.append(" DESC");
|
||||
|
||||
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||
|
||||
List<PollData> polls = new ArrayList<>();
|
||||
|
||||
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString())) {
|
||||
if (resultSet == null)
|
||||
return polls;
|
||||
|
||||
do {
|
||||
String pollName = resultSet.getString(1);
|
||||
String description = resultSet.getString(2);
|
||||
byte[] creatorPublicKey = resultSet.getBytes(3);
|
||||
String owner = resultSet.getString(4);
|
||||
long published = resultSet.getLong(5);
|
||||
|
||||
String optionsSql = "SELECT option_name FROM PollOptions WHERE poll_name = ? ORDER BY option_index ASC";
|
||||
try (ResultSet optionsResultSet = this.repository.checkedExecute(optionsSql, pollName)) {
|
||||
if (optionsResultSet == null)
|
||||
return null;
|
||||
|
||||
List<PollOptionData> pollOptions = new ArrayList<>();
|
||||
|
||||
// NOTE: do-while because checkedExecute() above has already called rs.next() for us
|
||||
do {
|
||||
String optionName = optionsResultSet.getString(1);
|
||||
|
||||
pollOptions.add(new PollOptionData(optionName));
|
||||
} while (optionsResultSet.next());
|
||||
|
||||
polls.add(new PollData(creatorPublicKey, owner, pollName, description, pollOptions, published));
|
||||
}
|
||||
|
||||
} while (resultSet.next());
|
||||
|
||||
return polls;
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch polls from repository", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PollData fromPollName(String pollName) throws DataException {
|
||||
String sql = "SELECT description, creator, owner, published_when FROM Polls WHERE poll_name = ?";
|
||||
|
@@ -31,7 +31,7 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
|
||||
|
||||
int version = resultSet.getInt(1);
|
||||
int nonce = resultSet.getInt(2);
|
||||
Service service = Service.valueOf(resultSet.getInt(3));
|
||||
int serviceInt = resultSet.getInt(3);
|
||||
int size = resultSet.getInt(4);
|
||||
boolean isDataRaw = resultSet.getBoolean(5); // NOT NULL, so no null to false
|
||||
DataType dataType = isDataRaw ? DataType.RAW_DATA : DataType.DATA_HASH;
|
||||
@@ -44,7 +44,7 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
|
||||
ArbitraryTransactionData.Compression compression = ArbitraryTransactionData.Compression.valueOf(resultSet.getInt(12));
|
||||
|
||||
List<PaymentData> payments = this.getPaymentsFromSignature(baseTransactionData.getSignature());
|
||||
return new ArbitraryTransactionData(baseTransactionData, version, service, nonce, size, name,
|
||||
return new ArbitraryTransactionData(baseTransactionData, version, serviceInt, nonce, size, name,
|
||||
identifier, method, secret, compression, data, dataType, metadataHash, payments);
|
||||
} catch (SQLException e) {
|
||||
throw new DataException("Unable to fetch arbitrary transaction from repository", e);
|
||||
@@ -66,7 +66,7 @@ public class HSQLDBArbitraryTransactionRepository extends HSQLDBTransactionRepos
|
||||
HSQLDBSaver saveHelper = new HSQLDBSaver("ArbitraryTransactions");
|
||||
|
||||
saveHelper.bind("signature", arbitraryTransactionData.getSignature()).bind("sender", arbitraryTransactionData.getSenderPublicKey())
|
||||
.bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getService().value)
|
||||
.bind("version", arbitraryTransactionData.getVersion()).bind("service", arbitraryTransactionData.getServiceInt())
|
||||
.bind("nonce", arbitraryTransactionData.getNonce()).bind("size", arbitraryTransactionData.getSize())
|
||||
.bind("is_data_raw", arbitraryTransactionData.getDataType() == DataType.RAW_DATA).bind("data", arbitraryTransactionData.getData())
|
||||
.bind("metadata_hash", arbitraryTransactionData.getMetadataHash()).bind("name", arbitraryTransactionData.getName())
|
||||
|
@@ -61,6 +61,7 @@ public class Settings {
|
||||
|
||||
// Common to all networking (API/P2P)
|
||||
private String bindAddress = "::"; // Use IPv6 wildcard to listen on all local addresses
|
||||
private String bindAddressFallback = "0.0.0.0"; // Some systems are unable to bind using IPv6
|
||||
|
||||
// UI servers
|
||||
private int uiPort = 12388;
|
||||
@@ -104,6 +105,7 @@ public class Settings {
|
||||
private Integer gatewayPort;
|
||||
private boolean gatewayEnabled = false;
|
||||
private boolean gatewayLoggingEnabled = false;
|
||||
private boolean gatewayLoopbackEnabled = false;
|
||||
|
||||
// Specific to this node
|
||||
private boolean wipeUnconfirmedOnStart = false;
|
||||
@@ -178,6 +180,8 @@ public class Settings {
|
||||
private boolean archiveEnabled = true;
|
||||
/** How often to attempt archiving (ms). */
|
||||
private long archiveInterval = 7171L; // milliseconds
|
||||
/** Serialization version to use when building an archive */
|
||||
private int defaultArchiveVersion = 1;
|
||||
|
||||
|
||||
/** Whether to automatically bootstrap instead of syncing from genesis */
|
||||
@@ -215,7 +219,7 @@ public class Settings {
|
||||
public long recoveryModeTimeout = 10 * 60 * 1000L;
|
||||
|
||||
/** Minimum peer version number required in order to sync with them */
|
||||
private String minPeerVersion = "3.8.2";
|
||||
private String minPeerVersion = "3.8.7";
|
||||
/** Whether to allow connections with peers below minPeerVersion
|
||||
* If true, we won't sync with them but they can still sync with us, and will show in the peers list
|
||||
* If false, sync will be blocked both ways, and they will not appear in the peers list */
|
||||
@@ -273,6 +277,7 @@ public class Settings {
|
||||
private String[] bootstrapHosts = new String[] {
|
||||
"http://bootstrap.qortal.org",
|
||||
"http://bootstrap2.qortal.org",
|
||||
"http://bootstrap3.qortal.org",
|
||||
"http://bootstrap.qortal.online"
|
||||
};
|
||||
|
||||
@@ -350,7 +355,7 @@ public class Settings {
|
||||
private Long maxStorageCapacity = null;
|
||||
|
||||
/** Whether to serve QDN data without authentication */
|
||||
private boolean qdnAuthBypassEnabled = false;
|
||||
private boolean qdnAuthBypassEnabled = true;
|
||||
|
||||
// Domain mapping
|
||||
public static class DomainMap {
|
||||
@@ -633,6 +638,10 @@ public class Settings {
|
||||
return this.gatewayLoggingEnabled;
|
||||
}
|
||||
|
||||
public boolean isGatewayLoopbackEnabled() {
|
||||
return this.gatewayLoopbackEnabled;
|
||||
}
|
||||
|
||||
|
||||
public boolean getWipeUnconfirmedOnStart() {
|
||||
return this.wipeUnconfirmedOnStart;
|
||||
@@ -681,6 +690,10 @@ public class Settings {
|
||||
return this.bindAddress;
|
||||
}
|
||||
|
||||
public String getBindAddressFallback() {
|
||||
return this.bindAddressFallback;
|
||||
}
|
||||
|
||||
public boolean isUPnPEnabled() {
|
||||
return this.uPnPEnabled;
|
||||
}
|
||||
@@ -926,6 +939,10 @@ public class Settings {
|
||||
return this.archiveInterval;
|
||||
}
|
||||
|
||||
public int getDefaultArchiveVersion() {
|
||||
return this.defaultArchiveVersion;
|
||||
}
|
||||
|
||||
|
||||
public boolean getBootstrap() {
|
||||
return this.bootstrap;
|
||||
@@ -994,6 +1011,10 @@ public class Settings {
|
||||
}
|
||||
|
||||
public boolean isQDNAuthBypassEnabled() {
|
||||
if (this.gatewayEnabled) {
|
||||
// We must always bypass QDN authentication in gateway mode, in order for it to function properly
|
||||
return true;
|
||||
}
|
||||
return this.qdnAuthBypassEnabled;
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import org.qortal.account.Account;
|
||||
import org.qortal.block.BlockChain;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.PaymentData;
|
||||
@@ -32,7 +33,7 @@ public class ArbitraryTransaction extends Transaction {
|
||||
private ArbitraryTransactionData arbitraryTransactionData;
|
||||
|
||||
// Other useful constants
|
||||
public static final int MAX_DATA_SIZE = 4000;
|
||||
public static final int MAX_DATA_SIZE = 256;
|
||||
public static final int MAX_METADATA_LENGTH = 32;
|
||||
public static final int HASH_LENGTH = TransactionTransformer.SHA256_LENGTH;
|
||||
public static final int MAX_IDENTIFIER_LENGTH = 64;
|
||||
@@ -87,6 +88,12 @@ public class ArbitraryTransaction extends Transaction {
|
||||
if (this.transactionData.getFee() < 0)
|
||||
return ValidationResult.NEGATIVE_FEE;
|
||||
|
||||
// After the feature trigger, we require the fee to be sufficient if it's not 0.
|
||||
// If the fee is zero, then the nonce is validated in isSignatureValid() as an alternative to a fee
|
||||
if (this.arbitraryTransactionData.getTimestamp() >= BlockChain.getInstance().getArbitraryOptionalFeeTimestamp() && this.arbitraryTransactionData.getFee() != 0L) {
|
||||
return super.isFeeValid();
|
||||
}
|
||||
|
||||
return ValidationResult.OK;
|
||||
}
|
||||
|
||||
@@ -207,10 +214,14 @@ public class ArbitraryTransaction extends Transaction {
|
||||
// Clear nonce from transactionBytes
|
||||
ArbitraryTransactionTransformer.clearNonce(transactionBytes);
|
||||
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
// As of feature-trigger timestamp, we only require a nonce when the fee is zero
|
||||
boolean beforeFeatureTrigger = this.arbitraryTransactionData.getTimestamp() < BlockChain.getInstance().getArbitraryOptionalFeeTimestamp();
|
||||
if (beforeFeatureTrigger || this.arbitraryTransactionData.getFee() == 0L) {
|
||||
// We only need to check nonce for recent transactions due to PoW verification overhead
|
||||
if (NTP.getTime() - this.arbitraryTransactionData.getTimestamp() < HISTORIC_THRESHOLD) {
|
||||
int difficulty = ArbitraryDataManager.getInstance().getPowDifficulty();
|
||||
return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, difficulty, nonce);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +252,15 @@ public class ArbitraryTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
ArbitraryTransactionData arbitraryTransactionData = (ArbitraryTransactionData) transactionData;
|
||||
if (arbitraryTransactionData.getName() == null)
|
||||
return;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(arbitraryTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -5,6 +5,7 @@ import java.util.List;
|
||||
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.repository.NamesDatabaseIntegrityCheck;
|
||||
import org.qortal.data.naming.NameData;
|
||||
import org.qortal.data.transaction.CancelSellNameTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -81,7 +82,13 @@ public class CancelSellNameTransaction extends Transaction {
|
||||
|
||||
@Override
|
||||
public void preProcess() throws DataException {
|
||||
// Nothing to do
|
||||
CancelSellNameTransactionData cancelSellNameTransactionData = (CancelSellNameTransactionData) transactionData;
|
||||
|
||||
// Rebuild this name in the Names table from the transaction history
|
||||
// This is necessary because in some rare cases names can be missing from the Names table after registration
|
||||
// but we have been unable to reproduce the issue and track down the root cause
|
||||
NamesDatabaseIntegrityCheck namesDatabaseIntegrityCheck = new NamesDatabaseIntegrityCheck();
|
||||
namesDatabaseIntegrityCheck.rebuildName(cancelSellNameTransactionData.getName(), this.repository);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -8,6 +8,7 @@ import java.util.function.Predicate;
|
||||
import org.qortal.account.Account;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.arbitrary.ArbitraryDataStorageManager;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.crypto.MemoryPoW;
|
||||
import org.qortal.data.naming.NameData;
|
||||
@@ -22,6 +23,7 @@ import org.qortal.settings.Settings;
|
||||
import org.qortal.transform.TransformationException;
|
||||
import org.qortal.transform.transaction.ChatTransactionTransformer;
|
||||
import org.qortal.transform.transaction.TransactionTransformer;
|
||||
import org.qortal.utils.ListUtils;
|
||||
import org.qortal.utils.NTP;
|
||||
|
||||
public class ChatTransaction extends Transaction {
|
||||
@@ -156,8 +158,7 @@ public class ChatTransaction extends Transaction {
|
||||
}
|
||||
|
||||
// Check for blocked author by address
|
||||
ResourceListManager listManager = ResourceListManager.getInstance();
|
||||
if (listManager.listContains("blockedAddresses", this.chatTransactionData.getSender(), true)) {
|
||||
if (ListUtils.isAddressBlocked(this.chatTransactionData.getSender())) {
|
||||
return ValidationResult.ADDRESS_BLOCKED;
|
||||
}
|
||||
|
||||
@@ -166,7 +167,7 @@ public class ChatTransaction extends Transaction {
|
||||
if (names != null && names.size() > 0) {
|
||||
for (NameData nameData : names) {
|
||||
if (nameData != null && nameData.getName() != null) {
|
||||
if (listManager.listContains("blockedNames", nameData.getName(), false)) {
|
||||
if (ListUtils.isNameBlocked(nameData.getName())) {
|
||||
return ValidationResult.NAME_BLOCKED;
|
||||
}
|
||||
}
|
||||
|
@@ -312,16 +312,24 @@ public class BlockTransformer extends Transformer {
|
||||
ByteArrayOutputStream atHashBytes = new ByteArrayOutputStream(atBytesLength);
|
||||
long atFees = 0;
|
||||
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
if (block.getAtStatesHash() != null) {
|
||||
// We already have the AT states hash
|
||||
atFees = blockData.getATFees();
|
||||
atHashBytes.write(block.getAtStatesHash());
|
||||
}
|
||||
else {
|
||||
// We need to build the AT states hash
|
||||
for (ATStateData atStateData : block.getATStates()) {
|
||||
// Skip initial states generated by DEPLOY_AT transactions in the same block
|
||||
if (atStateData.isInitial())
|
||||
continue;
|
||||
|
||||
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
|
||||
atHashBytes.write(atStateData.getStateHash());
|
||||
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
atHashBytes.write(atStateData.getATAddress().getBytes(StandardCharsets.UTF_8));
|
||||
atHashBytes.write(atStateData.getStateHash());
|
||||
atHashBytes.write(Longs.toByteArray(atStateData.getFees()));
|
||||
|
||||
atFees += atStateData.getFees();
|
||||
atFees += atStateData.getFees();
|
||||
}
|
||||
}
|
||||
|
||||
bytes.write(Ints.toByteArray(blockData.getATCount()));
|
||||
|
@@ -7,7 +7,6 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Utf8;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
@@ -131,7 +130,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
|
||||
payments.add(PaymentTransformer.fromByteBuffer(byteBuffer));
|
||||
}
|
||||
|
||||
Service service = Service.valueOf(byteBuffer.getInt());
|
||||
int service = byteBuffer.getInt();
|
||||
|
||||
// We might be receiving hash of data instead of actual raw data
|
||||
boolean isRaw = byteBuffer.get() != 0;
|
||||
@@ -226,7 +225,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
|
||||
for (PaymentData paymentData : payments)
|
||||
bytes.write(PaymentTransformer.toBytes(paymentData));
|
||||
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getService().value));
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getServiceInt()));
|
||||
|
||||
bytes.write((byte) (arbitraryTransactionData.getDataType() == DataType.RAW_DATA ? 1 : 0));
|
||||
|
||||
@@ -299,7 +298,7 @@ public class ArbitraryTransactionTransformer extends TransactionTransformer {
|
||||
bytes.write(PaymentTransformer.toBytes(paymentData));
|
||||
}
|
||||
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getService().value));
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getServiceInt()));
|
||||
|
||||
bytes.write(Ints.toByteArray(arbitraryTransactionData.getData().length));
|
||||
|
||||
|
@@ -3,11 +3,11 @@ package org.qortal.utils;
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.qortal.arbitrary.ArbitraryDataFile;
|
||||
import org.qortal.arbitrary.ArbitraryDataFileChunk;
|
||||
import org.qortal.arbitrary.ArbitraryDataReader;
|
||||
import org.qortal.arbitrary.ArbitraryDataResource;
|
||||
import org.qortal.arbitrary.*;
|
||||
import org.qortal.arbitrary.metadata.ArbitraryDataTransactionMetadata;
|
||||
import org.qortal.arbitrary.misc.Service;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceInfo;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceMetadata;
|
||||
import org.qortal.data.arbitrary.ArbitraryResourceStatus;
|
||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
@@ -110,13 +110,8 @@ public class ArbitraryTransactionUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] digest = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
byte[] signature = transactionData.getSignature();
|
||||
|
||||
// Load complete file and chunks
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
return arbitraryDataFile.allChunksExist();
|
||||
}
|
||||
@@ -126,18 +121,13 @@ public class ArbitraryTransactionUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] digest = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
byte[] signature = transactionData.getSignature();
|
||||
|
||||
if (metadataHash == null) {
|
||||
if (transactionData.getMetadataHash() == null) {
|
||||
// This file doesn't have any metadata/chunks, which means none exist
|
||||
return false;
|
||||
}
|
||||
|
||||
// Load complete file and chunks
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
return arbitraryDataFile.anyChunksExist();
|
||||
}
|
||||
@@ -147,12 +137,7 @@ public class ArbitraryTransactionUtils {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] digest = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
byte[] signature = transactionData.getSignature();
|
||||
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
// Find the folder containing the files
|
||||
Path parentPath = arbitraryDataFile.getFilePath().getParent();
|
||||
@@ -180,20 +165,15 @@ public class ArbitraryTransactionUtils {
|
||||
return 0;
|
||||
}
|
||||
|
||||
byte[] digest = transactionData.getData();
|
||||
byte[] metadataHash = transactionData.getMetadataHash();
|
||||
byte[] signature = transactionData.getSignature();
|
||||
|
||||
if (metadataHash == null) {
|
||||
if (transactionData.getMetadataHash() == null) {
|
||||
// This file doesn't have any metadata, therefore it has a single (complete) chunk
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load complete file and chunks
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(digest, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(transactionData);
|
||||
|
||||
return arbitraryDataFile.chunkCount() + 1; // +1 for the metadata file
|
||||
return arbitraryDataFile.fileCount();
|
||||
}
|
||||
|
||||
public static boolean isFileRecent(Path filePath, long now, long cleanupAfter) {
|
||||
@@ -243,31 +223,24 @@ public class ArbitraryTransactionUtils {
|
||||
}
|
||||
|
||||
public static void deleteCompleteFileAndChunks(ArbitraryTransactionData arbitraryTransactionData) throws DataException {
|
||||
byte[] completeHash = arbitraryTransactionData.getData();
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(completeHash, signature);
|
||||
arbitraryDataFile.setMetadataHash(metadataHash);
|
||||
arbitraryDataFile.deleteAll();
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
arbitraryDataFile.deleteAll(true);
|
||||
}
|
||||
|
||||
public static void convertFileToChunks(ArbitraryTransactionData arbitraryTransactionData, long now, long cleanupAfter) throws DataException {
|
||||
byte[] completeHash = arbitraryTransactionData.getData();
|
||||
byte[] metadataHash = arbitraryTransactionData.getMetadataHash();
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
|
||||
// Find the expected chunk hashes
|
||||
ArbitraryDataFile expectedDataFile = ArbitraryDataFile.fromHash(completeHash, signature);
|
||||
expectedDataFile.setMetadataHash(metadataHash);
|
||||
ArbitraryDataFile expectedDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
|
||||
if (metadataHash == null || !expectedDataFile.getMetadataFile().exists()) {
|
||||
if (arbitraryTransactionData.getMetadataHash() == null || !expectedDataFile.getMetadataFile().exists()) {
|
||||
// We don't have the metadata file, or this transaction doesn't have one - nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] completeHash = arbitraryTransactionData.getData();
|
||||
byte[] signature = arbitraryTransactionData.getSignature();
|
||||
|
||||
// Split the file into chunks
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromHash(completeHash, signature);
|
||||
ArbitraryDataFile arbitraryDataFile = ArbitraryDataFile.fromTransactionData(arbitraryTransactionData);
|
||||
int chunkCount = arbitraryDataFile.split(ArbitraryDataFile.CHUNK_SIZE);
|
||||
if (chunkCount > 1) {
|
||||
LOGGER.info(String.format("Successfully split %s into %d chunk%s",
|
||||
@@ -426,7 +399,7 @@ public class ArbitraryTransactionUtils {
|
||||
|
||||
// If "build" has been specified, build the resource before returning its status
|
||||
if (build != null && build == true) {
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, null);
|
||||
ArbitraryDataReader reader = new ArbitraryDataReader(name, ArbitraryDataFile.ResourceIdType.NAME, service, identifier);
|
||||
try {
|
||||
if (!reader.isBuilding()) {
|
||||
reader.loadSynchronously(false);
|
||||
@@ -440,4 +413,41 @@ public class ArbitraryTransactionUtils {
|
||||
return resource.getStatus(false);
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> addStatusToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Determine and add the status of each resource
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
try {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ArbitraryDataFile.ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryResourceStatus status = resource.getStatus(true);
|
||||
if (status != null) {
|
||||
resourceInfo.status = status;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
|
||||
} catch (Exception e) {
|
||||
// Catch and log all exceptions, since some systems are experiencing 500 errors when including statuses
|
||||
LOGGER.info("Caught exception when adding status to resource {}: {}", resourceInfo, e.toString());
|
||||
}
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
public static List<ArbitraryResourceInfo> addMetadataToResources(List<ArbitraryResourceInfo> resources) {
|
||||
// Add metadata fields to each resource if they exist
|
||||
List<ArbitraryResourceInfo> updatedResources = new ArrayList<>();
|
||||
for (ArbitraryResourceInfo resourceInfo : resources) {
|
||||
ArbitraryDataResource resource = new ArbitraryDataResource(resourceInfo.name, ArbitraryDataFile.ResourceIdType.NAME,
|
||||
resourceInfo.service, resourceInfo.identifier);
|
||||
ArbitraryDataTransactionMetadata transactionMetadata = resource.getLatestTransactionMetadata();
|
||||
ArbitraryResourceMetadata resourceMetadata = ArbitraryResourceMetadata.fromTransactionMetadata(transactionMetadata, false);
|
||||
if (resourceMetadata != null) {
|
||||
resourceInfo.metadata = resourceMetadata;
|
||||
}
|
||||
updatedResources.add(resourceInfo);
|
||||
}
|
||||
return updatedResources;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -21,6 +21,16 @@ public class BlockArchiveUtils {
|
||||
* into the HSQLDB, in order to make it SQL-compatible
|
||||
* again.
|
||||
* <p>
|
||||
* This is only fully compatible with archives that use
|
||||
* serialization version 1. For version 2 (or above),
|
||||
* we are unable to import individual AT states as we
|
||||
* only have a single combined hash, so the use cases
|
||||
* for this are greatly limited.
|
||||
* <p>
|
||||
* A version 1 archive should ultimately be rebuildable
|
||||
* via a resync or reindex from genesis, allowing
|
||||
* access to this feature once again.
|
||||
* <p>
|
||||
* Note: calls discardChanges() and saveChanges(), so
|
||||
* make sure that you commit any existing repository
|
||||
* changes before calling this method.
|
||||
@@ -61,9 +71,18 @@ public class BlockArchiveUtils {
|
||||
repository.getBlockRepository().save(blockInfo.getBlockData());
|
||||
|
||||
// Save AT state data hashes
|
||||
for (ATStateData atStateData : blockInfo.getAtStates()) {
|
||||
atStateData.setHeight(blockInfo.getBlockData().getHeight());
|
||||
repository.getATRepository().save(atStateData);
|
||||
if (blockInfo.getAtStates() != null) {
|
||||
for (ATStateData atStateData : blockInfo.getAtStates()) {
|
||||
atStateData.setHeight(blockInfo.getBlockData().getHeight());
|
||||
repository.getATRepository().save(atStateData);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We don't have AT state hashes, so we are only importing a partial state.
|
||||
// This can still be useful to allow orphaning to very old blocks, when we
|
||||
// need to access other chainstate info (such as balances) at an earlier block.
|
||||
// In order to do this, the orphan process must be temporarily adjusted to avoid
|
||||
// orphaning AT states, as it will otherwise fail due to having no previous state.
|
||||
}
|
||||
|
||||
} catch (DataException e) {
|
||||
|
@@ -241,13 +241,48 @@ public class FilesystemUtils {
|
||||
String[] files = ArrayUtils.removeElement(path.toFile().list(), ".qortal");
|
||||
if (files.length == 1) {
|
||||
Path filePath = Paths.get(path.toString(), files[0]);
|
||||
data = Files.readAllBytes(filePath);
|
||||
if (filePath.toFile().isFile()) {
|
||||
data = Files.readAllBytes(filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* isSingleFileResource
|
||||
* Returns true if the path points to a file, or a
|
||||
* directory containing a single file only.
|
||||
*
|
||||
* @param path to file or directory
|
||||
* @param excludeQortalDirectory - if true, a directory containing a single file and a .qortal directory is considered a single file resource
|
||||
* @return
|
||||
* @throws IOException
|
||||
*/
|
||||
public static boolean isSingleFileResource(Path path, boolean excludeQortalDirectory) {
|
||||
// If the path is a file, read the contents directly
|
||||
if (path.toFile().isFile()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Or if it's a directory, only load file contents if there is a single file inside it
|
||||
else if (path.toFile().isDirectory()) {
|
||||
String[] files = path.toFile().list();
|
||||
if (excludeQortalDirectory) {
|
||||
files = ArrayUtils.removeElement(files, ".qortal");
|
||||
}
|
||||
if (files.length == 1) {
|
||||
Path filePath = Paths.get(path.toString(), files[0]);
|
||||
if (filePath.toFile().isFile()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static byte[] readFromFile(String filePath, long position, int size) throws IOException {
|
||||
RandomAccessFile file = new RandomAccessFile(filePath, "r");
|
||||
file.seek(position);
|
||||
|
38
src/main/java/org/qortal/utils/ListUtils.java
Normal file
38
src/main/java/org/qortal/utils/ListUtils.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package org.qortal.utils;
|
||||
|
||||
import org.qortal.list.ResourceListManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ListUtils {
|
||||
|
||||
/* Blocking */
|
||||
|
||||
public static List<String> blockedNames() {
|
||||
return ResourceListManager.getInstance().getStringsInListsWithPrefix("blockedNames");
|
||||
}
|
||||
|
||||
public static boolean isNameBlocked(String name) {
|
||||
return ResourceListManager.getInstance().listWithPrefixContains("blockedNames", name, false);
|
||||
}
|
||||
|
||||
public static boolean isAddressBlocked(String address) {
|
||||
return ResourceListManager.getInstance().listWithPrefixContains("blockedAddresses", address, true);
|
||||
}
|
||||
|
||||
|
||||
/* Following */
|
||||
|
||||
public static List<String> followedNames() {
|
||||
return ResourceListManager.getInstance().getStringsInListsWithPrefix("followedNames");
|
||||
}
|
||||
|
||||
public static boolean isFollowingName(String name) {
|
||||
return ResourceListManager.getInstance().listWithPrefixContains("followedNames", name, false);
|
||||
}
|
||||
|
||||
public static int followedNamesCount() {
|
||||
return ListUtils.followedNames().size();
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user