diff --git a/.classpath b/.classpath
index f0257c5a54142160c682eb3de6435e2e5ec2d03a..cd377e474d1ca053f6d38878c601a5b9f5a669ab 100644
--- a/.classpath
+++ b/.classpath
@@ -23,21 +23,9 @@
 			<attribute name="maven.pomderived" value="true"/>
 		</attributes>
 	</classpathentry>
-	<classpathentry kind="src" path="target/generated-sources/annotations">
+	<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
 		<attributes>
-			<attribute name="optional" value="true"/>
 			<attribute name="maven.pomderived" value="true"/>
-			<attribute name="ignore_optional_problems" value="true"/>
-			<attribute name="m2e-apt" value="true"/>
-		</attributes>
-	</classpathentry>
-	<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
-		<attributes>
-			<attribute name="optional" value="true"/>
-			<attribute name="maven.pomderived" value="true"/>
-			<attribute name="ignore_optional_problems" value="true"/>
-			<attribute name="m2e-apt" value="true"/>
-			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>
 	<classpathentry kind="output" path="target/classes"/>
diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs
index f9fe34593fcd3624a964478aeb438b0d44fe7237..839d647eef851c560a9854ff81d9caa1df594ced 100644
--- a/.settings/org.eclipse.core.resources.prefs
+++ b/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
 encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/pom.xml b/pom.xml
index 9656bff7670bd6f797feb5664e7dc5ef0f9da1fb..5ad3481c8f2092dfe5803665c14402978c360118 100644
--- a/pom.xml
+++ b/pom.xml
@@ -14,6 +14,7 @@
 		<maven.compiler.source>1.8</maven.compiler.source>
 		<maven.compiler.target>1.8</maven.compiler.target>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss z</maven.build.timestamp.format>
 		<skipTests>true</skipTests>
 		<mexplorer.version>1.3.27</mexplorer.version>
 		<jre8mac64file>jre-8u201-macosx-x64</jre8mac64file>
@@ -51,7 +52,6 @@
 	</dependencies>
 	<build>
 		<plugins>
-
 			<plugin>
 				<groupId>org.codehaus.mojo</groupId>
 				<artifactId>wagon-maven-plugin</artifactId>
@@ -119,6 +119,28 @@
 				<artifactId>maven-antrun-plugin</artifactId>
 				<version>1.8</version>
 				<executions>
+					<execution>
+						<id>prepare-resources</id>
+						<phase>generate-resources</phase>
+						<configuration>
+							<target>
+								<mkdir
+									dir="${project.build.directory}/classes/unimelb/mf/client" />
+								<copy
+									file="${project.basedir}/src/main/resources/unimelb/mf/client/app.properties"
+									tofile="${project.build.directory}/classes/unimelb/mf/client/app.properties" />
+								<replace
+									file="${project.build.directory}/classes/unimelb/mf/client/app.properties"
+									token="@VERSION@" value="${project.version}" />
+								<replace
+									file="${project.build.directory}/classes/unimelb/mf/client/app.properties"
+									token="@BUILD_TIME@" value="${maven.build.timestamp}" />
+							</target>
+						</configuration>
+						<goals>
+							<goal>run</goal>
+						</goals>
+					</execution>
 					<execution>
 						<phase>package</phase>
 						<configuration>
@@ -344,5 +366,41 @@
 				</dependencies>
 			</plugin>
 		</plugins>
+		<pluginManagement>
+			<plugins>
+				<!--This plugin's configuration is used to store Eclipse m2e settings 
+					only. It has no influence on the Maven build itself. -->
+				<plugin>
+					<groupId>org.eclipse.m2e</groupId>
+					<artifactId>lifecycle-mapping</artifactId>
+					<version>1.0.0</version>
+					<configuration>
+						<lifecycleMappingMetadata>
+							<pluginExecutions>
+								<pluginExecution>
+									<pluginExecutionFilter>
+										<groupId>
+											org.apache.maven.plugins
+										</groupId>
+										<artifactId>
+											maven-antrun-plugin
+										</artifactId>
+										<versionRange>
+											[1.8,)
+										</versionRange>
+										<goals>
+											<goal>run</goal>
+										</goals>
+									</pluginExecutionFilter>
+									<action>
+										<execute />
+									</action>
+								</pluginExecution>
+							</pluginExecutions>
+						</lifecycleMappingMetadata>
+					</configuration>
+				</plugin>
+			</plugins>
+		</pluginManagement>
 	</build>
 </project>
diff --git a/src/main/java/unimelb/mf/client/App.java b/src/main/java/unimelb/mf/client/App.java
new file mode 100644
index 0000000000000000000000000000000000000000..5aad90b22cd7eb700cd9deb603c29d6c5cf1b64d
--- /dev/null
+++ b/src/main/java/unimelb/mf/client/App.java
@@ -0,0 +1,35 @@
+package unimelb.mf.client;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+public final class App {
+
+    private App() {
+    }
+
+    private static Properties _properties = null;
+
+    static void loadProperties() {
+        if (_properties == null) {
+            _properties = new Properties();
+            try (InputStream in = App.class.getResourceAsStream("app.properties")) {
+                _properties.load(in);
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    public static String version() {
+        loadProperties();
+        return _properties.getProperty("version");
+    }
+
+    public static String buildTime() {
+        loadProperties();
+        return _properties.getProperty("buildTime");
+    }
+
+}
diff --git a/src/main/java/unimelb/mf/client/sync/MFSyncApp.java b/src/main/java/unimelb/mf/client/sync/MFSyncApp.java
index ffdd0d43028dd1f0d4fddbc1f3961256947efc65..412ab8e18301775c94a99a5e01324f7528ec2721 100644
--- a/src/main/java/unimelb/mf/client/sync/MFSyncApp.java
+++ b/src/main/java/unimelb/mf/client/sync/MFSyncApp.java
@@ -9,6 +9,7 @@ import java.net.InetAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketException;
+import java.nio.file.FileVisitOption;
 import java.nio.file.FileVisitResult;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -16,6 +17,7 @@ import java.nio.file.SimpleFileVisitor;
 import java.nio.file.attribute.BasicFileAttributes;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.Timer;
 import java.util.TimerTask;
@@ -29,6 +31,7 @@ import java.util.logging.Level;
 
 import arc.xml.XmlDoc;
 import arc.xml.XmlStringWriter;
+import unimelb.mf.client.App;
 import unimelb.mf.client.file.PosixAttributes;
 import unimelb.mf.client.sync.check.AssetItem;
 import unimelb.mf.client.sync.check.ChecksumType;
@@ -566,48 +569,51 @@ public abstract class MFSyncApp extends AbstractMFApp<unimelb.mf.client.sync.set
         if (job instanceof DirectoryUploadCheckJob) {
             DirectoryUploadCheckJob ucj = (DirectoryUploadCheckJob) job;
             List<Path> files = new ArrayList<Path>(settings().batchSize());
-            Files.walkFileTree(ucj.srcDirectory(), new SimpleFileVisitor<Path>() {
-                @Override
-                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                    try {
-                        if (ucj.accept(file)) {
-                            files.add(file);
-                            if (files.size() >= settings().batchSize()) {
-                                // check files
-                                _queriers.submit(new FileSetCheckTask(session(), logger(), new ArrayList<Path>(files),
-                                        ucj, settings().csumCheck(), settings().checkHandler(), _workers));
-                                files.clear();
+            Files.walkFileTree(ucj.srcDirectory(), settings().followLinks() ? EnumSet.of(FileVisitOption.FOLLOW_LINKS)
+                    : EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
+                        @Override
+                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                            try {
+                                if (ucj.accept(file)) {
+                                    files.add(file);
+                                    if (files.size() >= settings().batchSize()) {
+                                        // check files
+                                        _queriers.submit(new FileSetCheckTask(session(), logger(),
+                                                new ArrayList<Path>(files), ucj, settings().csumCheck(),
+                                                settings().checkHandler(), _workers));
+                                        files.clear();
+                                    }
+                                }
+                            } catch (Throwable e) {
+                                if (e instanceof InterruptedException) {
+                                    Thread.currentThread().interrupt();
+                                    return FileVisitResult.TERMINATE;
+                                }
+                                logger().log(Level.SEVERE, e.getMessage(), e);
                             }
+                            return FileVisitResult.CONTINUE;
                         }
-                    } catch (Throwable e) {
-                        if (e instanceof InterruptedException) {
-                            Thread.currentThread().interrupt();
-                            return FileVisitResult.TERMINATE;
-                        }
-                        logger().log(Level.SEVERE, e.getMessage(), e);
-                    }
-                    return FileVisitResult.CONTINUE;
-                }
 
-                @Override
-                public FileVisitResult visitFileFailed(Path file, IOException ioe) {
-                    logger().log(Level.SEVERE, "Failed to access file: " + file, ioe);
-                    return FileVisitResult.CONTINUE;
-                }
+                        @Override
+                        public FileVisitResult visitFileFailed(Path file, IOException ioe) {
+                            logger().log(Level.SEVERE, "Failed to access file: " + file, ioe);
+                            return FileVisitResult.CONTINUE;
+                        }
 
-                @Override
-                public FileVisitResult postVisitDirectory(Path dir, IOException ioe) {
-                    if (ioe != null) {
-                        logger().log(Level.SEVERE, ioe.getMessage(), ioe);
-                    }
-                    return FileVisitResult.CONTINUE;
-                }
+                        @Override
+                        public FileVisitResult postVisitDirectory(Path dir, IOException ioe) {
+                            if (ioe != null) {
+                                logger().log(Level.SEVERE, ioe.getMessage(), ioe);
+                            }
+                            return FileVisitResult.CONTINUE;
+                        }
 
-                @Override
-                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
-                    return super.preVisitDirectory(dir, attrs);
-                }
-            });
+                        @Override
+                        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+                                throws IOException {
+                            return super.preVisitDirectory(dir, attrs);
+                        }
+                    });
             if (!files.isEmpty()) {
                 // check files
                 _queriers.submit(new FileSetCheckTask(session(), logger(), new ArrayList<Path>(files), ucj,
@@ -647,47 +653,50 @@ public abstract class MFSyncApp extends AbstractMFApp<unimelb.mf.client.sync.set
             duj.resolveDstAssetNamespaceStore(session());
 
             List<Path> files = new ArrayList<Path>(settings().batchSize());
-            Files.walkFileTree(duj.srcDirectory(), new SimpleFileVisitor<Path>() {
-                @Override
-                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                    try {
-                        if (duj.accept(file)) {
-                            files.add(file);
-                            if (files.size() >= settings().batchSize()) {
-                                _queriers.submit(new FileSetUploadTask(session(), logger(), new ArrayList<Path>(files),
-                                        duj, settings().csumCheck(), settings().retry(), _ul, _workers));
-                                files.clear();
+            Files.walkFileTree(duj.srcDirectory(), settings().followLinks() ? EnumSet.of(FileVisitOption.FOLLOW_LINKS)
+                    : EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
+                        @Override
+                        public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                            try {
+                                if (duj.accept(file)) {
+                                    files.add(file);
+                                    if (files.size() >= settings().batchSize()) {
+                                        _queriers.submit(new FileSetUploadTask(session(), logger(),
+                                                new ArrayList<Path>(files), duj, settings().csumCheck(),
+                                                settings().retry(), _ul, _workers));
+                                        files.clear();
+                                    }
+                                }
+                            } catch (Throwable e) {
+                                if (e instanceof InterruptedException) {
+                                    Thread.currentThread().interrupt();
+                                    return FileVisitResult.TERMINATE;
+                                }
+                                logger().log(Level.SEVERE, e.getMessage(), e);
                             }
+                            return FileVisitResult.CONTINUE;
                         }
-                    } catch (Throwable e) {
-                        if (e instanceof InterruptedException) {
-                            Thread.currentThread().interrupt();
-                            return FileVisitResult.TERMINATE;
-                        }
-                        logger().log(Level.SEVERE, e.getMessage(), e);
-                    }
-                    return FileVisitResult.CONTINUE;
-                }
 
-                @Override
-                public FileVisitResult visitFileFailed(Path file, IOException ioe) {
-                    logger().log(Level.SEVERE, "Failed to access file: " + file, ioe);
-                    return FileVisitResult.CONTINUE;
-                }
+                        @Override
+                        public FileVisitResult visitFileFailed(Path file, IOException ioe) {
+                            logger().log(Level.SEVERE, "Failed to access file: " + file, ioe);
+                            return FileVisitResult.CONTINUE;
+                        }
 
-                @Override
-                public FileVisitResult postVisitDirectory(Path dir, IOException ioe) {
-                    if (ioe != null) {
-                        logger().log(Level.SEVERE, ioe.getMessage(), ioe);
-                    }
-                    return FileVisitResult.CONTINUE;
-                }
+                        @Override
+                        public FileVisitResult postVisitDirectory(Path dir, IOException ioe) {
+                            if (ioe != null) {
+                                logger().log(Level.SEVERE, ioe.getMessage(), ioe);
+                            }
+                            return FileVisitResult.CONTINUE;
+                        }
 
-                @Override
-                public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
-                    return super.preVisitDirectory(dir, attrs);
-                }
-            });
+                        @Override
+                        public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
+                                throws IOException {
+                            return super.preVisitDirectory(dir, attrs);
+                        }
+                    });
             if (!files.isEmpty()) {
                 _queriers.submit(new FileSetUploadTask(session(), logger(), new ArrayList<Path>(files), duj,
                         settings().csumCheck(), settings().retry(), _ul, _workers));
@@ -827,4 +836,8 @@ public abstract class MFSyncApp extends AbstractMFApp<unimelb.mf.client.sync.set
         }
     }
 
+    public void printVersion() {
+        System.out.println(String.format("%s %s (build-time: %s)", this.applicationName(), App.version(), App.buildTime()));
+    }
+
 }
diff --git a/src/main/java/unimelb/mf/client/sync/cli/MFCheck.java b/src/main/java/unimelb/mf/client/sync/cli/MFCheck.java
index ea2629d8377ecb57e33b414d4cefa0e1830e58cf..b984499a6920cd735c53368f67663a78a08802ec 100644
--- a/src/main/java/unimelb/mf/client/sync/cli/MFCheck.java
+++ b/src/main/java/unimelb/mf/client/sync/cli/MFCheck.java
@@ -77,6 +77,7 @@ public class MFCheck extends MFSyncApp {
         System.out.println("    --batch-size <size>                       Size of the query result. Defaults to " + unimelb.mf.client.sync.settings.Settings.DEFAULT_BATCH_SIZE);
         System.out.println("    --quiet                                   Do not print progress messages.");
         System.out.println("    --help                                    Prints usage.");
+        System.out.println("    --version                                 Prints version.");
         System.out.println();
         System.out.println("POSITIONAL ARGUMENTS:");
         System.out.println("    <dir>                                     Local directory path.");
@@ -101,6 +102,10 @@ public class MFCheck extends MFSyncApp {
                         printUsage();
                         System.exit(0);
                     }
+                    if ("--version".equalsIgnoreCase(args[i])) {
+                        printVersion();
+                        System.exit(0);
+                    }
                     int n = parseMFOptions(args, i);
                     if (n > 0) {
                         i += n;
diff --git a/src/main/java/unimelb/mf/client/sync/cli/MFDownload.java b/src/main/java/unimelb/mf/client/sync/cli/MFDownload.java
index ea8597d74e001056ba81cb2e0fedf64d4f446699..979804324a4ce922d4ac9148144deabb609223b4 100644
--- a/src/main/java/unimelb/mf/client/sync/cli/MFDownload.java
+++ b/src/main/java/unimelb/mf/client/sync/cli/MFDownload.java
@@ -76,6 +76,7 @@ public class MFDownload extends MFSyncApp {
         System.out.println("    --sync-delete-files                       Delete local files that do not have corresponding assets exist on the server side.");
         System.out.println("    --quiet                                   Do not print progress messages.");
         System.out.println("    --help                                    Prints usage.");
+        System.out.println("    --version                                 Prints version.");
         System.out.println();
         System.out.println("POSITIONAL ARGUMENTS:");
         System.out.println("    <namespace>                               The asset namespace to download.");
@@ -102,6 +103,10 @@ public class MFDownload extends MFSyncApp {
                 printUsage();
                 System.exit(args.length > 1 ? 1 : 0);
             }
+            if ("--version".equalsIgnoreCase(arg)) {
+                printVersion();
+                System.exit(args.length > 1 ? 1 : 0);
+            }
         }
 
         if (args.length == 2 && "--config".equalsIgnoreCase(args[0])) {
@@ -324,10 +329,10 @@ public class MFDownload extends MFSyncApp {
         } else if ("--log-dir".equalsIgnoreCase(args[i])) {
             Path logDir = Paths.get(args[i + 1]).toAbsolutePath();
             if (!Files.exists(logDir)) {
-                throw new IllegalArgumentException("Directory: " + args[i] + " does not exist.");
+                throw new IllegalArgumentException("Directory: " + args[i + 1] + " does not exist.");
             }
             if (!Files.isDirectory(logDir)) {
-                throw new IllegalArgumentException(args[i] + " is not a directory.");
+                throw new IllegalArgumentException(args[i + 1] + " is not a directory.");
             }
             settings().setLogDirectory(logDir);
             return 2;
diff --git a/src/main/java/unimelb/mf/client/sync/cli/MFUpload.java b/src/main/java/unimelb/mf/client/sync/cli/MFUpload.java
index 70c9b6d7d2589c2611146e932c0fd5e1ccfa99dc..ad7d04351c80cbd912e535408fdcce0c87f6f156 100644
--- a/src/main/java/unimelb/mf/client/sync/cli/MFUpload.java
+++ b/src/main/java/unimelb/mf/client/sync/cli/MFUpload.java
@@ -73,6 +73,7 @@ public class MFUpload extends MFSyncApp {
         System.out.println("    --hard-delete-assets                      Force the asset deletion (see --sync-delete-assets) process to hard delete assets.  Otherwise, the behaviour is controlled by server properties (whether a deletion is a soft or hard action).");
         System.out.println("    --quiet                                   Do not print progress messages.");
         System.out.println("    --help                                    Prints usage.");
+        System.out.println("    --version                                 Prints version.");
         System.out.println();
         System.out.println("POSITIONAL ARGUMENTS:");
         System.out.println("    src-dir                                   Source directory to upload.");
@@ -101,6 +102,10 @@ public class MFUpload extends MFSyncApp {
                 printUsage();
                 System.exit(args.length > 1 ? 1 : 0);
             }
+            if ("--version".equalsIgnoreCase(arg)) {
+                printVersion();
+                System.exit(args.length > 1 ? 1 : 0);
+            }
         }
 
         if (args.length == 2 && "--config".equalsIgnoreCase(args[0])) {
@@ -336,10 +341,10 @@ public class MFUpload extends MFSyncApp {
         } else if ("--log-dir".equalsIgnoreCase(args[i])) {
             Path logDir = Paths.get(args[i + 1]).toAbsolutePath();
             if (!Files.exists(logDir)) {
-                throw new IllegalArgumentException("Directory: " + args[i] + " does not exist.");
+                throw new IllegalArgumentException("Directory: " + args[i + 1] + " does not exist.");
             }
             if (!Files.isDirectory(logDir)) {
-                throw new IllegalArgumentException(args[i] + " is not a directory.");
+                throw new IllegalArgumentException(args[i + 1] + " is not a directory.");
             }
             settings().setLogDirectory(logDir);
             return 2;
diff --git a/src/main/java/unimelb/mf/client/sync/settings/Settings.java b/src/main/java/unimelb/mf/client/sync/settings/Settings.java
index 591b835a0144f5dc15a86a7220313e7447fe1447..daa6a63b7543d0edd7c5d50792f0f2a97aac54fb 100644
--- a/src/main/java/unimelb/mf/client/sync/settings/Settings.java
+++ b/src/main/java/unimelb/mf/client/sync/settings/Settings.java
@@ -72,6 +72,8 @@ public class Settings implements MFApp.Settings {
 
     private boolean _hardDestroyAssets = false;
 
+    private boolean _followLinks = false;
+
     public Settings() {
         _jobs = new ArrayList<Job>();
     }
@@ -489,4 +491,11 @@ public class Settings implements MFApp.Settings {
         return (hasUploadJobs()) && deleteAssets();
     }
 
+    public boolean followLinks() {
+        return _followLinks;
+    }
+
+    public void setFollowLinks(boolean followLinks) {
+        _followLinks = followLinks;
+    }
 }
diff --git a/src/main/resources/unimelb/mf/client/app.properties b/src/main/resources/unimelb/mf/client/app.properties
new file mode 100644
index 0000000000000000000000000000000000000000..d9acd6ffdf3ba971c9ea53f52bcbeae8397a7acd
--- /dev/null
+++ b/src/main/resources/unimelb/mf/client/app.properties
@@ -0,0 +1,3 @@
+version=@VERSION@
+buildTime=@BUILD_TIME@
+
diff --git a/src/main/scripts/unix/namespace-download-shell-script-url-create b/src/main/scripts/unix/namespace-download-shell-script-url-create
new file mode 100755
index 0000000000000000000000000000000000000000..be1a9f637f41b2bea7274265727598a069b9e341
--- /dev/null
+++ b/src/main/scripts/unix/namespace-download-shell-script-url-create
@@ -0,0 +1,156 @@
+#!/bin/bash
+
+# Default values for the script arguments
+EXPIRE_DAYS=14
+OVERWRITE=false
+VERBOSE=true
+
+
+# aterm.jar download url
+ATERM_URL=https://mediaflux.researchsoftware.unimelb.edu.au/mflux/aterm.jar
+
+# function to print usage
+usage() {
+    echo ""
+	echo "Usage:"
+	echo "    $(basename $0) [-h|--help] [--expire-days <number-of-days>] [--ncsr <ncsr>] [--overwrite] [--quiet] <namespace>"
+	echo ""
+	echo "Options:"
+	echo "    -h | --help                       prints usage."
+	echo "    --email <addresses>               specify the email recipient of the generated url. Can be comma-separated if there are more than one."
+	echo "    --expire-days <number-of-days>    expiry of the auth token. Defaults to ${EXPIRE_DAYS} days."
+	echo "    --overwrite                       overwrite if output file exists."
+	echo "    --quiet                           do not print output message."
+	echo ""
+	echo "Positional arguments:"
+	echo "    <namespace>                       Mediaflux asset namespace to be downloaded by the scripts. Can be multiple, but must be from the same project."
+	echo ""
+	echo "Examples:"
+	echo "    $(basename $0) --email user1@unimelb.edu.au --expire-days 10 proj-abc-1128.4.999/RAW_DATA proj-abc-1128.4.999/PROCESSED_DATA"
+	echo ""
+}
+
+# check java
+[[ -z $(which java) ]] && echo "Error: cannot find java." 1>&2 && exit 1
+
+# check mflux.cfg
+[[ -z $MFLUX_CFG || ! -f $MFLUX_CFG ]] && MFLUX_CFG="./mflux.cfg"
+[[ -z $MFLUX_CFG || ! -f $MFLUX_CFG ]] && MFLUX_CFG="${HOME}/.Arcitecta/mflux.cfg"
+[[ -z $MFLUX_CFG || ! -f $MFLUX_CFG ]] && MFLUX_CFG="$(dirname ${BASH_SOURCE[0]})/../../config/mflux.cfg"
+[[ -z $MFLUX_CFG || ! -f $MFLUX_CFG ]] && MFLUX_CFG="$(dirname ${BASH_SOURCE[0]})/../../mflux.cfg"
+[[ -z $MFLUX_CFG || ! -f $MFLUX_CFG ]] && MFLUX_CFG="$(dirname ${BASH_SOURCE[0]})/mflux.cfg"
+[[ -z $MFLUX_CFG || ! -f $MFLUX_CFG ]] && echo "Error: cannot find ${HOME}/.Arcitecta/mflux.cfg." 1>&2 && exit 1
+
+# check aterm.jar
+[[ -z $MFLUX_ATERM || ! -f $MFLUX_ATERM ]] && MFLUX_ATERM="$(dirname ${BASH_SOURCE[0]})/../../lib/aterm.jar"
+[[ -z $MFLUX_ATERM || ! -f $MFLUX_ATERM ]] && MFLUX_ATERM="${HOME}/.Arcitecta/aterm.jar"
+[[ -z $MFLUX_ATERM || ! -f $MFLUX_ATERM ]] && MFLUX_ATERM=/opt/mediaflux/bin/aterm.jar
+[[ -z $MFLUX_ATERM || ! -f $MFLUX_ATERM ]] && MFLUX_ATERM=./aterm.jar
+
+
+# download aterm.jar
+if [[ -z $MFLUX_ATERM || ! -f $MFLUX_ATERM ]]; then
+    MFLUX_ATERM=${HOME}/.Arcitecta/aterm.jar
+    mkdir -p ${HOME}/.Arcitecta
+    CURL=$(which curl)
+    WGET=$(which wget)
+    [[ -z "${CURL}" && -z "${WGET}" ]] && echo "Error: cannot download aterm.jar. Found no curl or wget." 1>&2 && exit 1
+    if [[ ! -z "${CURL}" ]]; then
+        curl -f --create-dirs -k -o "$(dirname $0)/aterm.jar" "${ATERM_URL}"
+    else
+        wget --no-check-certificate -O "$(dirname $0)/aterm.jar" "${ATERM_URL}"
+    fi
+    if [[ $? -ne 0 ]]; then
+        echo "Error: failed to download aterm.jar"
+        exit 1
+    fi
+fi
+
+ATERM="java -jar -Dmf.cfg=$MFLUX_CFG $MFLUX_ATERM nogui"
+SERVICE=unimelb.asset.download.shell.script.url.create 
+COMMAND="${ATERM} ${SERVICE}"
+
+##
+## parse arguments
+##
+declare -a NAMESPACES=()
+declare -a EMAILS=()
+while [[ $# -gt 0 ]]
+do
+key="$1"
+case $key in
+    --expire-days)
+    EXPIRE_DAYS="$2"
+    shift
+    shift
+    ;;
+    --email)
+    IFS=',' read -r -a EMAILS <<< "$2"; unset IFS
+    shift
+    shift
+    ;;
+    --overwrite)
+    OVERWRITE=true
+    shift
+    ;;
+    --quiet)
+    VERBOSE=false
+    shift
+    ;;
+    -h|--help)
+    shift
+    usage && exit 0
+    ;;
+    *)
+    NAMESPACES+=("$1")
+    shift
+    ;;
+esac
+done
+
+if [[ ${#NAMESPACES[@]} -eq 0 ]]; then
+    echo "Error: no Mediaflux asset namespace(directory) is specified." 1>&2
+    usage
+    exit 1
+fi
+
+COMMAND="${COMMAND} :download <"
+for ns in "${NAMESPACES[@]}"
+do
+    # resolve project id from namespace path
+    prj=$(echo "${ns}" | egrep -o "proj-.*-[0-9]+(.[0-9]+)*")
+    if [[ -z ${prj} ]]; then
+        echo "Error: cannot resolve project id from namespace path: %{ns}." 1>&2
+        exit 1
+    fi
+    
+    # if multiple namespaces specified, check if they're from same project.
+    if [[ -z ${PROJECT} ]]; then
+        PROJECT=${prj}
+        ROLE="${PROJECT}:participant-a"
+        ROLE_NAMESPACE="${PROJECT}:"
+    else
+        if [[ ${prj} != ${PROJECT} ]]; then
+            echo "Error: you cannot specify namespaces from multiple projects." 1>&2
+            usage
+            exit 1
+        fi
+    fi
+    COMMAND="${COMMAND} :namespace ${ns}"
+done
+COMMAND="${COMMAND} :token < :role -type role ${ROLE} :to now+${EXPIRE_DAYS}day > :verbose ${VERBOSE} :overwrite ${OVERWRITE}"
+COMMAND="${COMMAND} >"
+
+COMMAND="${COMMAND} :token < :perm < :resource -type role:namespace ${ROLE_NAMESPACE} :access ADMINISTER > >"
+
+if [[ ${#EMAILS[@]} -gt 0 ]]; then
+    COMMAND="${COMMAND} :email < "
+    for email in "${EMAILS[@]}"
+    do
+        COMMAND="${COMMAND} :to ${email}"
+    done
+    COMMAND="${COMMAND} >"
+fi
+
+# execute the command to generate the url
+${COMMAND}