├── examples
└── helloworld
│ ├── .gitignore
│ ├── spk
│ ├── scripts
│ │ ├── postinst
│ │ ├── postuninst
│ │ ├── postupgrade
│ │ ├── preinst
│ │ ├── preuninst
│ │ ├── preupgrade
│ │ └── start-stop-status
│ └── wizard
│ │ └── install_uifile
│ ├── app
│ ├── bin
│ │ └── helloworld.sh
│ └── dsm
│ │ ├── index.html
│ │ ├── images
│ │ ├── icon_64.png
│ │ └── icon_256.png
│ │ └── config
│ ├── makefile
│ ├── ivy.xml
│ └── build.xml
├── .gitignore
├── src
└── main
│ ├── resources
│ └── net
│ │ └── filebot
│ │ └── ant
│ │ └── spk
│ │ └── antlib.xml
│ └── java
│ └── net
│ └── filebot
│ └── ant
│ └── spk
│ ├── Info.java
│ ├── Icon.java
│ ├── Compression.java
│ ├── util
│ └── Digest.java
│ ├── SPK.java
│ ├── pgp
│ ├── OpenPGPSecretKey.java
│ └── OpenPGPSignature.java
│ ├── CodeSignTask.java
│ ├── PackageTask.java
│ └── RepositoryTask.java
├── makefile
├── README.md
└── LICENSE
/examples/helloworld/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | lib/
3 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/postinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exit 0
3 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/postuninst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exit 0
3 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/postupgrade:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exit 0
3 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/preinst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exit 0
3 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/preuninst:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exit 0
3 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/preupgrade:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | exit 0
3 |
--------------------------------------------------------------------------------
/examples/helloworld/app/bin/helloworld.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "Hello World!"
3 |
--------------------------------------------------------------------------------
/examples/helloworld/makefile:
--------------------------------------------------------------------------------
1 | ANT := ant -lib lib
2 |
3 | build:
4 | $(ANT) retrieve
5 | $(ANT) spk
6 |
--------------------------------------------------------------------------------
/examples/helloworld/app/dsm/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Hello World!
4 |
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | build
3 |
4 | .gradle
5 | .settings
6 | .project
7 | .classpath
8 |
9 | *.gpg
10 | *.properties
11 |
--------------------------------------------------------------------------------
/examples/helloworld/app/dsm/images/icon_64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rednoah/ant-spk/HEAD/examples/helloworld/app/dsm/images/icon_64.png
--------------------------------------------------------------------------------
/examples/helloworld/app/dsm/images/icon_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rednoah/ant-spk/HEAD/examples/helloworld/app/dsm/images/icon_256.png
--------------------------------------------------------------------------------
/examples/helloworld/app/dsm/config:
--------------------------------------------------------------------------------
1 | {
2 | ".url": {
3 | "org.example.HelloWorld": {
4 | "type": "legacy",
5 | "title": "Hello World",
6 | "icon": "images/icon_{0}.png",
7 | "url": "/webman/3rdparty/helloworld/index.html"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/examples/helloworld/spk/scripts/start-stop-status:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | case "$1" in
4 | start)
5 | exit 0
6 | ;;
7 |
8 | stop)
9 | exit 0
10 | ;;
11 |
12 | status)
13 | exit 0
14 | ;;
15 |
16 | log)
17 | exit 0
18 | ;;
19 |
20 | *)
21 | exit 1
22 | ;;
23 | esac
24 |
--------------------------------------------------------------------------------
/examples/helloworld/ivy.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/main/resources/net/filebot/ant/spk/antlib.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/examples/helloworld/spk/wizard/install_uifile:
--------------------------------------------------------------------------------
1 | [{
2 | "step_title": "Step 1",
3 | "items": [{
4 | "type": "textfield",
5 | "desc": "Please enter your Hello World message.",
6 | "subitems": [{
7 | "key": "HELLO_WORLD",
8 | "desc": "Message",
9 | "defaultValue": "Hello World!",
10 | "validator": {
11 | "minLength": 5
12 | }
13 | }]
14 | }]
15 | }]
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/Info.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | public class Info {
4 |
5 | public static final String NAME = "package";
6 | public static final String VERSION = "version";
7 | public static final String ARCH = "arch";
8 |
9 | String name;
10 | String value;
11 |
12 | public void setName(String name) {
13 | this.name = name;
14 | }
15 |
16 | public void setValue(String value) {
17 | this.value = value;
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/makefile:
--------------------------------------------------------------------------------
1 | GRADLE := docker run --rm -v "$(PWD)":/ant-spk -w /ant-spk gradle:6-jdk11 gradle
2 |
3 | jar:
4 | $(GRADLE) clean build
5 |
6 | example:
7 | cd "$(PWD)/examples/helloworld" && ant -lib "$(PWD)/build/libs" -lib "lib"
8 |
9 | deploy:
10 | $(GRADLE) clean uploadArchives
11 | # open "https://oss.sonatype.org/#stagingRepositories"
12 |
13 | eclipse:
14 | $(GRADLE) cleanEclipse eclipse
15 |
16 | clean:
17 | git reset --hard
18 | git pull
19 | git --no-pager log -1
20 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/Icon.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | import java.io.File;
4 |
5 | public class Icon {
6 |
7 | Integer size;
8 | File file;
9 |
10 | public void setSize(Integer size) {
11 | this.size = size;
12 | }
13 |
14 | public void setFile(File file) {
15 | this.file = file;
16 | }
17 |
18 | public String getPackageName() {
19 | if (size == null || size.equals(64) || size.equals(72)) {
20 | return "PACKAGE_ICON.PNG";
21 | } else {
22 | return "PACKAGE_ICON_" + size + ".PNG";
23 | }
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/Compression.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | import org.apache.tools.ant.taskdefs.Tar.TarCompressionMethod;
4 |
5 | /**
6 | * In DSM 5.2 or older, package.tgz must be tgz format. In DSM 6.0 or newer, package.tgz can be tgz or xz format, but the file name must be package.tgz.
7 | */
8 | public enum Compression {
9 |
10 | none, gzip, xz;
11 |
12 | public TarCompressionMethod getTarCompressionMethod() {
13 | TarCompressionMethod compression = new TarCompressionMethod();
14 | compression.setValue(name());
15 | return compression;
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/util/Digest.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk.util;
2 |
3 | import java.io.File;
4 | import java.math.BigInteger;
5 | import java.nio.file.Files;
6 | import java.security.MessageDigest;
7 |
8 | import org.apache.tools.ant.BuildException;
9 |
10 | public class Digest {
11 |
12 | public static String md5(File file) {
13 | return digest(file, "MD5");
14 | }
15 |
16 | public static String sha256(File file) {
17 | return digest(file, "SHA-256");
18 | }
19 |
20 | public static String digest(File file, String algorithm) {
21 | try {
22 | MessageDigest digest = MessageDigest.getInstance(algorithm);
23 | digest.update(Files.readAllBytes(file.toPath()));
24 |
25 | // as hex string (e.g. 16 bytes = 32 hex digits)
26 | int digits = 2 * digest.getDigestLength();
27 | return String.format("%0" + digits + "x", new BigInteger(1, digest.digest()));
28 | } catch (Exception e) {
29 | throw new BuildException(e);
30 | }
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/SPK.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | import java.io.File;
4 | import java.net.URL;
5 | import java.util.ArrayList;
6 | import java.util.LinkedHashMap;
7 | import java.util.List;
8 | import java.util.Map;
9 |
10 | import org.apache.tools.ant.types.resources.URLResource;
11 |
12 | public class SPK {
13 |
14 | File file;
15 | URL url;
16 |
17 | public void setFile(File file) {
18 | this.file = file;
19 | }
20 |
21 | public void setUrl(URL url) {
22 | this.url = url;
23 | }
24 |
25 | Map infoList = new LinkedHashMap();
26 |
27 | public void addConfiguredInfo(Info info) {
28 | infoList.put(info.name, info.value);
29 | }
30 |
31 | List thumbnail = new ArrayList();
32 | List snapshot = new ArrayList();
33 |
34 | public void addConfiguredThumbnail(URLResource link) {
35 | thumbnail.add(link.getURL().toString());
36 | }
37 |
38 | public void addConfiguredSnapshot(URLResource link) {
39 | snapshot.add(link.getURL().toString());
40 | }
41 |
42 | }
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/pgp/OpenPGPSecretKey.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk.pgp;
2 |
3 | import java.io.IOException;
4 | import java.io.InputStream;
5 |
6 | import org.bouncycastle.openpgp.PGPObjectFactory;
7 | import org.bouncycastle.openpgp.PGPSecretKey;
8 | import org.bouncycastle.openpgp.PGPSecretKeyRing;
9 | import org.bouncycastle.openpgp.PGPUtil;
10 | import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
11 |
12 | public class OpenPGPSecretKey {
13 |
14 | private static final long MASK = 0xFFFFFFFFL;
15 |
16 | private PGPSecretKey secretKey;
17 | private char[] password;
18 |
19 | public OpenPGPSecretKey(String keyId, InputStream secretKeyRing, char[] password) throws IOException {
20 | PGPObjectFactory pgpObjectFactory = new BcPGPObjectFactory(PGPUtil.getDecoderStream(secretKeyRing));
21 |
22 | for (Object it = pgpObjectFactory.nextObject(); it != null; it = pgpObjectFactory.nextObject()) {
23 | PGPSecretKeyRing pgpSecretKeyRing = (PGPSecretKeyRing) it;
24 | PGPSecretKey pgpSecretKey = pgpSecretKeyRing.getSecretKey();
25 |
26 | if (keyId == null || keyId.isEmpty() || Long.valueOf(keyId, 16) == (pgpSecretKey.getKeyID() & MASK)) {
27 | this.secretKey = pgpSecretKey;
28 | break;
29 | }
30 | }
31 |
32 | // sanity check
33 | if (secretKey == null) {
34 | throw new IllegalArgumentException("Secret key " + keyId + " not found");
35 | }
36 |
37 | this.password = password;
38 | }
39 |
40 | public PGPSecretKey getSecretKey() {
41 | return secretKey;
42 | }
43 |
44 | public char[] getPassword() {
45 | return password;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/examples/helloworld/build.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ant-spk
2 | [](https://github.com/rednoah/ant-spk/releases)
3 | [](https://github.com/rednoah/ant-spk/releases)
4 |
5 | Ant Task for creating SPK packages for Synology NAS.
6 |
7 | ## Introduction
8 | I've found the Synology SDK tools for creating and signing SPK packages overly difficult to use and and terrible to automate. So here's an Apache Ant task to handle build automation of SPK packages in an easy to maintain and completely platform-independent manner.
9 |
10 | __Ant SPK Task__
11 | * Much more easy to use than whats in the official Synology SDK docs & tools
12 | * Automatically create and sign your SPK packages in your automated Ant build
13 | * Works on Windows, Mac and Linux (including Synology DSM) and any other device that can run Java 8
14 |
15 | Have a quick look at the [Synology DSM 3rd Party Apps Developer Guide](https://global.download.synology.com/download/Document/DeveloperGuide/DSM_Developer_Guide.pdf) and read the **Package Structure** section to learn more on how SPK packages work.
16 |
17 | ## Example
18 | ```xml
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | ```
47 |
48 | ## Downloads
49 | [ant-spk](https://github.com/rednoah/ant-spk) is available on [Maven Central](https://mvnrepository.com/artifact/net.filebot/ant-spk). Use [Apache Ivy](http://ant.apache.org/ivy/) to retrieve all the dependencies:
50 | ```xml
51 |
52 | ```
53 |
54 | ## Build
55 | [ant-spk](https://github.com/rednoah/ant-spk) uses the [Gradle 6.9](https://gradle.org/gradle-download/) build tool. Call `gradle example` to fetch all dependencies and build the example project.
56 |
57 | ## Real World Examples
58 | [ant-spk](https://github.com/rednoah/ant-spk) is used to automatically build `.spk` packages for the [FileBot](http://www.filebot.net/) project, so check out [filebot-node](https://github.com/filebot/filebot-node) [build.xml](https://github.com/filebot/filebot-node/blob/master/build.xml) or [java-installer](https://github.com/rednoah/java-installer) [build.xml](https://github.com/rednoah/java-installer/blob/master/build.xml) for a set of more comprehensive examples. 🚀
59 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/pgp/OpenPGPSignature.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk.pgp;
2 |
3 | import java.io.ByteArrayOutputStream;
4 | import java.io.File;
5 | import java.io.FileInputStream;
6 | import java.io.FileNotFoundException;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 | import java.security.Security;
10 | import java.security.SignatureException;
11 |
12 | import org.bouncycastle.bcpg.ArmoredOutputStream;
13 | import org.bouncycastle.bcpg.BCPGOutputStream;
14 | import org.bouncycastle.bcpg.HashAlgorithmTags;
15 | import org.bouncycastle.jce.provider.BouncyCastleProvider;
16 | import org.bouncycastle.openpgp.PGPException;
17 | import org.bouncycastle.openpgp.PGPPrivateKey;
18 | import org.bouncycastle.openpgp.PGPSignature;
19 | import org.bouncycastle.openpgp.PGPSignatureGenerator;
20 | import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor;
21 | import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
22 | import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
23 | import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
24 | import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
25 |
26 | public class OpenPGPSignature {
27 |
28 | static {
29 | Security.addProvider(new BouncyCastleProvider());
30 | }
31 |
32 | private PGPSignatureGenerator signature;
33 |
34 | public OpenPGPSignature(OpenPGPSecretKey key) throws PGPException {
35 | PGPDigestCalculatorProvider pgpDigestCalculator = new JcaPGPDigestCalculatorProviderBuilder().setProvider(BouncyCastleProvider.PROVIDER_NAME).build();
36 | PBESecretKeyDecryptor pbeSecretKeyDecryptor = new JcePBESecretKeyDecryptorBuilder(pgpDigestCalculator).setProvider(BouncyCastleProvider.PROVIDER_NAME).build(key.getPassword());
37 | JcaPGPContentSignerBuilder pgpContentSigner = new JcaPGPContentSignerBuilder(key.getSecretKey().getPublicKey().getAlgorithm(), HashAlgorithmTags.SHA1).setProvider(BouncyCastleProvider.PROVIDER_NAME).setDigestProvider(BouncyCastleProvider.PROVIDER_NAME);
38 |
39 | signature = new PGPSignatureGenerator(pgpContentSigner);
40 |
41 | PGPPrivateKey privateKey = key.getSecretKey().extractPrivateKey(pbeSecretKeyDecryptor);
42 | signature.init(PGPSignature.BINARY_DOCUMENT, privateKey);
43 | }
44 |
45 | public void update(byte[] buffer, int offset, int length) throws SignatureException {
46 | signature.update(buffer, offset, length);
47 | }
48 |
49 | public byte[] generate() throws IOException, SignatureException, PGPException {
50 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(1024);
51 |
52 | // make sure to call close() on these streams, because they will want to write some extra data at the end of the file
53 | try (BCPGOutputStream out = new BCPGOutputStream(new ArmoredOutputStream(buffer))) {
54 | signature.generate().encode(out);
55 | }
56 |
57 | return buffer.toByteArray();
58 | }
59 |
60 | public static OpenPGPSignature createSignatureGenerator(String keyId, File secring, char[] password) throws FileNotFoundException, IOException, PGPException {
61 | try (InputStream secretKeyRing = new FileInputStream(secring)) {
62 | OpenPGPSecretKey key = new OpenPGPSecretKey(keyId, secretKeyRing, password);
63 | OpenPGPSignature signature = new OpenPGPSignature(key);
64 | return signature;
65 | }
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/CodeSignTask.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | import static net.filebot.ant.spk.PackageTask.*;
4 |
5 | import java.io.File;
6 | import java.io.IOException;
7 | import java.io.InputStream;
8 | import java.nio.ByteBuffer;
9 | import java.nio.charset.StandardCharsets;
10 | import java.nio.file.Files;
11 | import java.nio.file.StandardCopyOption;
12 | import java.security.SignatureException;
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.TreeMap;
16 | import java.util.UUID;
17 |
18 | import org.apache.http.HttpEntity;
19 | import org.apache.http.HttpResponse;
20 | import org.apache.http.client.methods.HttpPost;
21 | import org.apache.http.entity.ContentType;
22 | import org.apache.http.entity.mime.MultipartEntityBuilder;
23 | import org.apache.http.impl.client.CloseableHttpClient;
24 | import org.apache.http.impl.client.HttpClientBuilder;
25 | import org.apache.tools.ant.BuildException;
26 | import org.apache.tools.ant.Task;
27 | import org.apache.tools.ant.taskdefs.Tar.TarFileSet;
28 | import org.apache.tools.ant.types.Resource;
29 | import org.bouncycastle.openpgp.PGPException;
30 |
31 | import net.filebot.ant.spk.pgp.OpenPGPSignature;
32 |
33 | public class CodeSignTask extends Task {
34 |
35 | String keyId;
36 | File secring;
37 |
38 | char[] password = new char[0]; // empty password by default
39 | String timestamp = "http://timestamp.synology.com/timestamp.php"; // default Synology signature server
40 |
41 | public void setKeyId(String keyId) {
42 | this.keyId = keyId;
43 | }
44 |
45 | public void setSecring(File secring) {
46 | this.secring = secring;
47 | }
48 |
49 | public void setPassword(String password) {
50 | this.password = password.toCharArray();
51 | }
52 |
53 | public void setTimestamp(String timestamp) {
54 | this.timestamp = timestamp;
55 | }
56 |
57 | File token = new File(SYNO_SIGNATURE);
58 | List cats = new ArrayList();
59 |
60 | public void setToken(File token) {
61 | this.token = token;
62 | }
63 |
64 | public void addConfiguredCat(TarFileSet files) {
65 | cats.add(files);
66 | }
67 |
68 | @Override
69 | public void execute() {
70 | byte[] asciiArmoredSignatureFile;
71 |
72 | // compute PGP signature
73 | log("GPG: sign with key " + keyId);
74 |
75 | try {
76 | OpenPGPSignature signature = OpenPGPSignature.createSignatureGenerator(keyId, secring, password);
77 |
78 | // cat files in case-sensitive alphabetical tar entry path order
79 | byte[] buffer = new byte[64 * 1024];
80 | int length = 0;
81 | for (Resource r : getTarOrderCatResources()) {
82 | try (InputStream in = r.getInputStream()) {
83 | while ((length = in.read(buffer, 0, buffer.length)) != -1) {
84 | signature.update(buffer, 0, length);
85 | }
86 | }
87 | }
88 |
89 | asciiArmoredSignatureFile = signature.generate();
90 | } catch (IOException | SignatureException | PGPException e) {
91 | throw new BuildException("Failed to compute PGP signature: " + e);
92 | }
93 |
94 | // sign the signature
95 | log("SYNO: Submit signature to " + timestamp);
96 |
97 | try (CloseableHttpClient httpClient = HttpClientBuilder.create().build()) {
98 | HttpPost httpPost = new HttpPost(timestamp);
99 |
100 | // timestamp.synology.com requires the full Content-Disposition head to be sent, including the filename section, e.g. Content-Disposition: form-data; name="file"; filename="ALLCAT.dat.asc"
101 | HttpEntity pastData = MultipartEntityBuilder.create().addBinaryBody("file", asciiArmoredSignatureFile, ContentType.DEFAULT_BINARY, UUID.randomUUID().toString()).build();
102 | httpPost.setEntity(pastData);
103 |
104 | HttpResponse response = httpClient.execute(httpPost);
105 | Files.copy(response.getEntity().getContent(), token.toPath(), StandardCopyOption.REPLACE_EXISTING);
106 | dumpSignature(Files.readAllBytes(token.toPath()));
107 | } catch (IOException e) {
108 | throw new BuildException("Failed to retrieve signature: " + e.getMessage());
109 | }
110 | }
111 |
112 | protected void dumpSignature(byte[] bytes) {
113 | log(StandardCharsets.UTF_8.decode(ByteBuffer.wrap(bytes)).toString());
114 | }
115 |
116 | protected Resource[] getTarOrderCatResources() {
117 | TreeMap sortedCats = new TreeMap();
118 | cats.forEach(fs -> {
119 | fs.forEach(r -> {
120 | sortedCats.put(getTarEntryName(r.getName(), fs), r);
121 | });
122 | });
123 | return sortedCats.values().toArray(new Resource[0]);
124 | }
125 |
126 | protected String getTarEntryName(String vPath, TarFileSet tarFileSet) {
127 | if (vPath.isEmpty() || vPath.startsWith("/")) {
128 | throw new IllegalArgumentException("Illegal tar entry: " + vPath);
129 | }
130 |
131 | String fullpath = tarFileSet.getFullpath(getProject());
132 | if (fullpath.length() > 0) {
133 | return fullpath;
134 | }
135 |
136 | String prefix = tarFileSet.getPrefix(getProject());
137 | if (prefix.length() > 0) {
138 | if (prefix.endsWith("/")) {
139 | return prefix + vPath;
140 | } else {
141 | return prefix + '/' + vPath;
142 | }
143 | }
144 |
145 | return vPath;
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/PackageTask.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | import static java.nio.charset.StandardCharsets.*;
4 | import static net.filebot.ant.spk.Info.*;
5 | import static net.filebot.ant.spk.util.Digest.*;
6 |
7 | import java.io.File;
8 | import java.nio.file.Files;
9 | import java.util.ArrayList;
10 | import java.util.LinkedHashMap;
11 | import java.util.List;
12 | import java.util.Map;
13 |
14 | import org.apache.tools.ant.BuildException;
15 | import org.apache.tools.ant.Task;
16 | import org.apache.tools.ant.taskdefs.Delete;
17 | import org.apache.tools.ant.taskdefs.Tar;
18 | import org.apache.tools.ant.taskdefs.Tar.TarFileSet;
19 | import org.apache.tools.ant.taskdefs.Tar.TarLongFileMode;
20 | import org.apache.tools.ant.types.FileSet;
21 |
22 | public class PackageTask extends Task {
23 |
24 | public static final String INFO = "INFO";
25 | public static final String SYNO_SIGNATURE = "syno_signature.asc";
26 |
27 | File destDir;
28 |
29 | Map infoList = new LinkedHashMap();
30 |
31 | List packageFiles = new ArrayList();
32 | List spkFiles = new ArrayList();
33 |
34 | Compression compression = Compression.gzip; // use GZIP by default, XZ requires DSM 6 or higher
35 |
36 | CodeSignTask codesign;
37 |
38 | public void setDestdir(File value) {
39 | destDir = value;
40 | }
41 |
42 | public void setCompression(Compression value) {
43 | compression = value;
44 | }
45 |
46 | public void setName(String value) {
47 | infoList.put(NAME, value);
48 | }
49 |
50 | public void setVersion(String value) {
51 | infoList.put(VERSION, value);
52 | }
53 |
54 | public void setArch(String value) {
55 | infoList.put(ARCH, value);
56 | }
57 |
58 | public void addConfiguredInfo(Info info) {
59 | infoList.put(info.name, info.value);
60 | }
61 |
62 | public void addConfiguredPackage(TarFileSet files) {
63 | packageFiles.add(files);
64 | }
65 |
66 | public void addConfiguredScripts(TarFileSet files) {
67 | files.setPrefix("scripts");
68 | spkFiles.add(files);
69 | }
70 |
71 | public void addConfiguredWizard(TarFileSet files) {
72 | files.setPrefix("WIZARD_UIFILES");
73 | spkFiles.add(files);
74 | }
75 |
76 | public void addConfiguredConf(TarFileSet files) {
77 | files.setPrefix("conf");
78 | spkFiles.add(files);
79 | }
80 |
81 | public void addConfiguredIcon(Icon icon) {
82 | TarFileSet files = new TarFileSet();
83 | files.setFullpath(icon.getPackageName());
84 | files.setFile(icon.file);
85 | spkFiles.add(files);
86 | }
87 |
88 | public void setLicense(File file) {
89 | TarFileSet files = new TarFileSet();
90 | files.setFullpath("LICENSE");
91 | files.setFile(file);
92 | spkFiles.add(files);
93 | }
94 |
95 | public void addConfiguredCodeSign(CodeSignTask codesign) {
96 | this.codesign = codesign;
97 | }
98 |
99 | @Override
100 | public void execute() throws BuildException {
101 | if (destDir == null || !infoList.containsKey(NAME) || !infoList.containsKey(VERSION) || !infoList.containsKey(ARCH))
102 | throw new BuildException("Required attributes: destdir, name, version, arch");
103 |
104 | if (packageFiles.isEmpty() || spkFiles.isEmpty())
105 | throw new BuildException("Required elements: package, scripts");
106 |
107 | String spkName = String.format("%s-%s-%s", infoList.get(NAME), infoList.get(VERSION), infoList.get(ARCH));
108 |
109 | File spkStaging = new File(destDir, spkName);
110 | File spkFile = new File(destDir, spkName + ".spk");
111 |
112 | // make sure staging folder exists
113 | spkStaging.mkdirs();
114 |
115 | // generate info and package files and add to spk fileset
116 | preparePackage(spkStaging);
117 | prepareInfo(spkStaging);
118 | prepareSignature(spkStaging);
119 |
120 | // spk must be an uncompressed tar
121 | tar(spkFile, Compression.none, spkFiles);
122 |
123 | // make sure staging folder is clean for next time
124 | clean(spkStaging);
125 | }
126 |
127 | private void prepareSignature(File tempDirectory) {
128 | if (codesign != null) {
129 | // select files that need to be signed
130 | spkFiles.forEach((fs) -> {
131 | fs.setProject(getProject());
132 | codesign.addConfiguredCat(fs);
133 | });
134 |
135 | // create signature file
136 | File signatureFile = new File(tempDirectory, SYNO_SIGNATURE);
137 | codesign.setToken(signatureFile);
138 |
139 | codesign.bindToOwner(this);
140 | codesign.execute();
141 |
142 | // add signature file to output package
143 | TarFileSet syno_signature = new TarFileSet();
144 | syno_signature.setFullpath(signatureFile.getName());
145 | syno_signature.setFile(signatureFile);
146 | spkFiles.add(syno_signature);
147 | }
148 | }
149 |
150 | private void preparePackage(File tempDirectory) {
151 | File packageFile = new File(tempDirectory, "package.tgz");
152 | tar(packageFile, compression, packageFiles);
153 |
154 | infoList.put("checksum", md5(packageFile));
155 |
156 | TarFileSet package_tgz = new TarFileSet();
157 | package_tgz.setFullpath(packageFile.getName());
158 | package_tgz.setFile(packageFile);
159 | spkFiles.add(package_tgz);
160 | }
161 |
162 | private void prepareInfo(File tempDirectory) {
163 | StringBuilder infoText = new StringBuilder();
164 | infoList.forEach((k, v) -> {
165 | infoText.append(k).append('=').append('"').append(v).append('"').append('\n');
166 | });
167 |
168 | File infoFile = new File(tempDirectory, INFO);
169 | log("Generating INFO: " + infoFile);
170 | try {
171 | Files.write(infoFile.toPath(), infoText.toString().getBytes(UTF_8));
172 | } catch (Exception e) {
173 | throw new BuildException("Failed to write INFO", e);
174 | }
175 |
176 | TarFileSet info = new TarFileSet();
177 | info.setFullpath(infoFile.getName());
178 | info.setFile(infoFile);
179 | spkFiles.add(info);
180 | }
181 |
182 | private void tar(File destFile, Compression compression, List files) {
183 | Tar tar = new Tar();
184 | tar.setProject(getProject());
185 | tar.setLocation(getLocation());
186 | tar.setTaskName(getTaskName());
187 | tar.setEncoding("utf-8");
188 |
189 | TarLongFileMode longFileMode = new TarLongFileMode();
190 | longFileMode.setValue("posix");
191 | tar.setLongfile(longFileMode);
192 |
193 | tar.setCompression(compression.getTarCompressionMethod());
194 |
195 | tar.setDestFile(destFile);
196 | for (FileSet fileset : files) {
197 | if (fileset != null) {
198 | // make sure the tarfileset element is initialized with all the project information it may need
199 | fileset.setProject(tar.getProject());
200 | fileset.setLocation(tar.getLocation());
201 | tar.add(fileset);
202 | }
203 | }
204 |
205 | tar.perform();
206 | }
207 |
208 | private void clean(File tempDirectory) {
209 | Delete cleanupTask = new Delete();
210 | cleanupTask.setProject(getProject());
211 | cleanupTask.setTaskName(getTaskName());
212 | cleanupTask.setLocation(getLocation());
213 | cleanupTask.setDir(tempDirectory);
214 | cleanupTask.perform();
215 | }
216 |
217 | }
218 |
--------------------------------------------------------------------------------
/src/main/java/net/filebot/ant/spk/RepositoryTask.java:
--------------------------------------------------------------------------------
1 | package net.filebot.ant.spk;
2 |
3 | import static java.nio.charset.StandardCharsets.*;
4 | import static java.util.Collections.*;
5 | import static net.filebot.ant.spk.PackageTask.*;
6 | import static net.filebot.ant.spk.util.Digest.*;
7 |
8 | import java.io.File;
9 | import java.io.InputStreamReader;
10 | import java.io.StringWriter;
11 | import java.nio.file.Files;
12 | import java.util.ArrayList;
13 | import java.util.LinkedHashMap;
14 | import java.util.LinkedHashSet;
15 | import java.util.List;
16 | import java.util.Map;
17 | import java.util.Set;
18 | import java.util.regex.Pattern;
19 |
20 | import javax.json.Json;
21 | import javax.json.JsonArrayBuilder;
22 | import javax.json.JsonObject;
23 | import javax.json.JsonObjectBuilder;
24 | import javax.json.JsonReader;
25 | import javax.json.JsonString;
26 | import javax.json.JsonValue;
27 | import javax.json.JsonWriter;
28 | import javax.json.stream.JsonGenerator;
29 |
30 | import org.apache.tools.ant.BuildException;
31 | import org.apache.tools.ant.Project;
32 | import org.apache.tools.ant.Task;
33 | import org.apache.tools.ant.taskdefs.Get;
34 | import org.apache.tools.ant.types.FileSet;
35 | import org.apache.tools.ant.types.Resource;
36 | import org.apache.tools.ant.types.ResourceCollection;
37 | import org.apache.tools.ant.types.TarFileSet;
38 | import org.apache.tools.ant.types.resources.URLResource;
39 | import org.apache.tools.ant.types.resources.Union;
40 | import org.apache.tools.ant.util.FileUtils;
41 |
42 | public class RepositoryTask extends Task {
43 |
44 | File index;
45 |
46 | Union keyrings = new Union();
47 | List spks = new ArrayList();
48 | List sources = new ArrayList();
49 |
50 | public void setFile(File file) {
51 | this.index = file;
52 | }
53 |
54 | public void addConfiguredKeyRing(FileSet key) {
55 | keyrings.add(key);
56 | }
57 |
58 | public void addConfiguredKeyRing(ResourceCollection key) {
59 | keyrings.add(key);
60 | }
61 |
62 | public void addConfiguredSource(URLResource source) {
63 | sources.add(source);
64 | }
65 |
66 | public void addConfiguredSPK(SPK spk) {
67 | if (spk.file == null) {
68 | throw new BuildException("Required attributes: [file] or [url, file]");
69 | }
70 |
71 | spks.add(spk);
72 | }
73 |
74 | public static final String KEYRINGS = "keyrings";
75 | public static final String PACKAGES = "packages";
76 |
77 | @Override
78 | public void execute() throws BuildException {
79 | if (index == null) {
80 | throw new BuildException("Required attributes: file");
81 | }
82 |
83 | try {
84 | // generate package source for spk files
85 | JsonArrayBuilder jsonPackages = Json.createArrayBuilder();
86 | getPackages().forEach((p) -> {
87 | JsonObjectBuilder jsonPackage = Json.createObjectBuilder();
88 | p.forEach((k, v) -> {
89 | if (v instanceof Boolean) {
90 | jsonPackage.add(k, (Boolean) v); // Boolean
91 | } else if (v instanceof Number) {
92 | jsonPackage.add(k, ((Number) v).longValue()); // Integer
93 | } else if (v instanceof String[]) {
94 | JsonArrayBuilder array = Json.createArrayBuilder(); // String Array
95 | for (String s : (String[]) v) {
96 | array.add(s);
97 | }
98 | jsonPackage.add(k, array);
99 | } else if (v == null) {
100 | jsonPackage.addNull(k); // null
101 | } else {
102 | jsonPackage.add(k, v.toString()); // String
103 | }
104 | });
105 | jsonPackages.add(jsonPackage);
106 | });
107 |
108 | // collect public keys and omit duplicates
109 | Set keyrings = new LinkedHashSet();
110 | keyrings.addAll(getKeyRings());
111 |
112 | // include keyrings and packages from external package sources
113 | for (URLResource source : sources) {
114 | try (JsonReader reader = Json.createReader(source.getInputStream())) {
115 | JsonObject json = reader.readObject();
116 |
117 | if (json.getJsonArray(KEYRINGS) != null) {
118 | for (JsonValue k : json.getJsonArray(KEYRINGS)) {
119 | log("Import keyring: " + source);
120 | keyrings.add(normalizeKey(((JsonString) k).getString()));
121 | }
122 | }
123 |
124 | if (json.getJsonArray(PACKAGES) != null) {
125 | for (JsonValue p : json.getJsonArray(PACKAGES)) {
126 | log("Import package: " + (((JsonString) ((JsonObject) p).get(PACKAGE))).getString());
127 | jsonPackages.add(p);
128 | }
129 | }
130 | } catch (Exception e) {
131 | throw new BuildException(e);
132 | }
133 | }
134 |
135 | JsonObjectBuilder jsonRoot = Json.createObjectBuilder();
136 | if (keyrings.size() > 0) {
137 | JsonArrayBuilder jsonKeyrings = Json.createArrayBuilder();
138 | keyrings.forEach(jsonKeyrings::add);
139 | jsonRoot.add(KEYRINGS, jsonKeyrings);
140 | }
141 | jsonRoot.add(PACKAGES, jsonPackages);
142 |
143 | log("Write Package Source: " + index);
144 | StringWriter json = new StringWriter();
145 | try (JsonWriter writer = Json.createWriterFactory(singletonMap(JsonGenerator.PRETTY_PRINTING, true)).createWriter(json)) {
146 | writer.writeObject(jsonRoot.build());
147 | }
148 | Files.write(index.toPath(), json.toString().trim().getBytes(UTF_8));
149 | } catch (Exception e) {
150 | throw new BuildException(e);
151 | }
152 | }
153 |
154 | public List getKeyRings() throws Exception {
155 | List keys = new ArrayList();
156 | for (Resource resource : keyrings) {
157 | log("Include keyring: " + resource.getName());
158 | String key = FileUtils.readFully(new InputStreamReader(resource.getInputStream(), US_ASCII));
159 | if (key != null) {
160 | keys.add(normalizeKey(key)); // make sure to normalize line endings
161 | }
162 | }
163 | return keys;
164 | }
165 |
166 | private static final Pattern NEWLINE = Pattern.compile("\\R");
167 | private static final String UNIX_NEWLINE = "\n";
168 |
169 | private String normalizeKey(String key) {
170 | return NEWLINE.matcher(key).replaceAll(UNIX_NEWLINE).trim();
171 | }
172 |
173 | public static final String PACKAGE = "package";
174 | public static final String VERSION = "version";
175 |
176 | public static final String LINK = "link";
177 | public static final String MD5 = "md5";
178 | public static final String SHA256 = "sha256"; // NOT SUPPORTED BY SYNOLOGY DSM
179 | public static final String SIZE = "size";
180 | public static final String THUMBNAIL = "thumbnail";
181 | public static final String SNAPSHOT = "snapshot";
182 |
183 | public List