diff --git a/src/main/java/unimelb/mf/client/archive/MFArchive.java b/src/main/java/unimelb/mf/client/archive/MFArchive.java index ccc81b59bfe7090e79198298bebf8ce574e9bfc1..f94aa40df01fabd97d8143363e2c3e8ae1a69a40 100644 --- a/src/main/java/unimelb/mf/client/archive/MFArchive.java +++ b/src/main/java/unimelb/mf/client/archive/MFArchive.java @@ -52,7 +52,7 @@ public class MFArchive { if (ext != null) { Type[] vs = values(); for (Type v : vs) { - if ("tgz".equalsIgnoreCase(ext)) { + if ("tgz".equalsIgnoreCase(ext) || "gtar".equalsIgnoreCase(ext) || "tar.gz".equalsIgnoreCase(ext)) { return GZIPPED_TAR; } else if (v.extension.equalsIgnoreCase(ext)) { return v; @@ -76,7 +76,8 @@ public class MFArchive { public static Type fromFileName(String fileName) { if (fileName != null) { - String ext = PathUtils.getFileExtension(fileName); + String ext = fileName.toLowerCase().endsWith(".tar.gz") ? "tar.gz" + : PathUtils.getFileExtension(fileName); return fromFileExtension(ext); } return null; diff --git a/src/main/java/unimelb/mf/client/sync/cli/MFImportArchive.java b/src/main/java/unimelb/mf/client/sync/cli/MFImportArchive.java index 5f0fa51b2d64e3b16d8aab2455c36018e0b2071e..99d69121e57b919aef5d281261952ead5e72a50c 100644 --- a/src/main/java/unimelb/mf/client/sync/cli/MFImportArchive.java +++ b/src/main/java/unimelb/mf/client/sync/cli/MFImportArchive.java @@ -1,55 +1,96 @@ package unimelb.mf.client.sync.cli; import java.nio.file.Path; -import java.util.List; +import java.util.Arrays; import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.logging.Logger; -import unimelb.mf.client.archive.MFArchive; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.Option; +import picocli.CommandLine.Parameters; +import unimelb.mf.client.session.MFConfigBuilder; +import unimelb.mf.client.session.MFSession; +import unimelb.mf.client.sync.task.FileSetArchiveCreateTask; +import unimelb.utils.LoggingUtils; +@Command(name = "mf-import-archive", abbreviateSynopsis = true, usageHelpWidth = 120, synopsisHeading = "\nUSAGE:\n ", descriptionHeading = "\nDESCRIPTION:\n ", description = "Import local files or directory into Mediaflux as an archive asset. The destination archive file format can be .zip, .tar or .aar.", parameterListHeading = "\nPARAMETERS:\n", optionListHeading = "\nOPTIONS:\n", sortOptions = false, version = MFImportArchive.VERSION, separator = " ") public class MFImportArchive implements Callable<Integer> { - public static class Options { - private MFArchive.Type archiveType = MFArchive.Type.ZIP; - private int compressLevel = 6; - private boolean analyze = false; + public static final String VERSION = "0.0.1"; - public Options(MFArchive.Type archiveType, int compressLevel, boolean analyze) { - this.archiveType = archiveType; - this.compressLevel = compressLevel; - this.analyze = analyze; - } + @Option(names = { "-c", + "--config" }, required = false, paramLabel = "<mflux.cfg>", description = "Mediaflux configuration file. If not specified, it will try $HOME/.Arcitecta/mflux.cfg on MacOS/Linux or %%USERPROFILE%%\\.Arcitecta\\mflux.cfg on Windows.") + private Path cfgFile; - public String archiveMimeType() { - if (this.archiveType != null) { - return this.archiveType.mimeType; - } - return null; - } + @Option(names = { "-a", + "--asset-path" }, required = true, paramLabel = "<asset-path>", description = "Path to the destination archive asset in Mediaflux.") + private String assetPath; + + @Option(names = { + "--log-dir" }, required = false, paramLabel = "<log-directory>", description = "The log directory. Logging is disabled unless this is specified.") + private Path logDir; + + @Option(names = { + "--compress" }, required = false, negatable = true, description = "Compress the destination archive. By default, the archive is not compressed.") + private boolean compress = false; + + @Option(names = { + "--analyze" }, required = false, negatable = true, description = "Analyze the asset content after the archive asset is created.") + private boolean analyze = false; + + @Option(names = { "-h", "--help" }, usageHelp = true, description = "Print usage information") + private boolean printHelp; + + @Option(names = { "--version" }, versionHelp = true, description = "Print version information") + private boolean printVersion; + + @Option(names = { "--quiet" }, defaultValue = "false", description = "Quiet mode. ") + private boolean quiet; + + @Parameters(description = "Source files or directories to import into Mediaflux.", index = "0", arity = "1..", paramLabel = "SRC_FILES") + private Path[] srcFiles; - public String archiveFileExtension() { - if (this.archiveType != null) { - return this.archiveType.extension; + @Override + public Integer call() throws Exception { + try { + MFConfigBuilder mfconfig = new MFConfigBuilder().setConsoleLogon(true); + mfconfig.findAndLoadFromConfigFile(); + mfconfig.loadFromSystemEnv(); + if (this.cfgFile != null) { + mfconfig.loadFromConfigFile(cfgFile); } - return null; - } + mfconfig.setApp(MFUpload.PROG); - public int compressLevel() { - return this.compressLevel; - } + MFSession session = MFSession.create(mfconfig); + + Logger logger = createLogger(this.logDir, "mf-import-archive", this.quiet ? Level.WARNING : Level.ALL, + 100000000, 2); - public boolean analyze() { - return this.analyze; + new FileSetArchiveCreateTask(session, logger, Arrays.asList(this.srcFiles), this.assetPath, + this.compress ? 6 : 0, this.analyze).execute(); + + return 0; + } catch (Throwable e) { + throw (e instanceof Exception) ? (Exception) e : new RuntimeException(e); } } - public MFImportArchive(List<Path> files, String assetPath) { - + private static Logger createLogger(Path logDir, String logFileName, Level logLevel, int logFileSize, + int logFileCount) throws Throwable { + Logger logger = logDir == null ? LoggingUtils.createConsoleLogger() + : LoggingUtils.createFileAndConsoleLogger(logDir, logFileName, logFileSize, logFileCount); + if (logLevel != null) { + logger.setLevel(logLevel); + } else { + logger.setLevel(Level.WARNING); + } + return logger; } - @Override - public Integer call() throws Exception { - // TODO Auto-generated method stub - return null; + public static void main(String[] args) { + System.exit(new CommandLine(new MFImportArchive()).execute(args)); } -} +} \ No newline at end of file diff --git a/src/main/java/unimelb/mf/client/sync/task/AggregatedUploadTask.java b/src/main/java/unimelb/mf/client/sync/task/AggregatedUploadTask.java index 7e7ea29fabdf7db3234f6cb37600d9434d186e0c..b1c5ee394ae0d81b16bd70f3dc6f8a9353133928 100644 --- a/src/main/java/unimelb/mf/client/sync/task/AggregatedUploadTask.java +++ b/src/main/java/unimelb/mf/client/sync/task/AggregatedUploadTask.java @@ -89,8 +89,8 @@ public class AggregatedUploadTask extends AbstractMFTask { private void doAggregateUpload() throws Throwable { - ServerClient.Input input = new MFGeneratedInput(MFArchive.TYPE_AAR, MFArchive.Type.AAR.extension, - null, -1, _store) { + ServerClient.Input input = new MFGeneratedInput(MFArchive.TYPE_AAR, MFArchive.Type.AAR.extension, null, -1, + _store == null ? null : ("asset:" + _store)) { @Override protected void consume(OutputStream out, AbortCheck ac) throws Throwable { Archive.declareSupportForAllTypes(); diff --git a/src/main/java/unimelb/mf/client/sync/task/FileSetArchiveCreateTask.java b/src/main/java/unimelb/mf/client/sync/task/FileSetArchiveCreateTask.java index 74f7b2dc7059a8d2958ec59c19a63913caf28aec..530d0818c9478b89aeac81740b24d9ee3409772d 100644 --- a/src/main/java/unimelb/mf/client/sync/task/FileSetArchiveCreateTask.java +++ b/src/main/java/unimelb/mf/client/sync/task/FileSetArchiveCreateTask.java @@ -1,49 +1,193 @@ package unimelb.mf.client.sync.task; +import java.io.IOException; import java.io.OutputStream; +import java.nio.file.FileVisitOption; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.EnumSet; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Level; import java.util.logging.Logger; +import arc.archive.ArchiveOutput; import arc.archive.ArchiveRegistry; import arc.mf.client.ServerClient; import arc.mf.client.archive.Archive; import arc.streams.StreamCopy.AbortCheck; -import arc.utils.Path; +import arc.xml.XmlStringWriter; import unimelb.mf.client.archive.MFArchive; +import unimelb.mf.client.session.MFGeneratedInput; import unimelb.mf.client.session.MFSession; import unimelb.mf.client.task.AbstractMFTask; import unimelb.mf.client.util.AssetNamespaceUtils; +import unimelb.mf.client.util.AssetUtils; +import unimelb.utils.PathUtils; public class FileSetArchiveCreateTask extends AbstractMFTask { - private List<Path> _files; - private String _dstAssetPath; - private boolean _createNS; - private int _compressLevel; + private Set<Path> _files; + private MFArchive.Type _archiveType; + private String _assetPath; + private int _compressionLevel = 6; + private boolean _analyze = false; - protected FileSetArchiveCreateTask(MFSession session, Logger logger, List<Path> files, String dstAssetPath, boolean createNS, int compressLevel) { + public FileSetArchiveCreateTask(MFSession session, Logger logger, List<Path> files, String assetPath, + int compressionLevel, boolean analyze) { super(session, logger); + _files = new LinkedHashSet<>(); + if (files != null && !files.isEmpty()) { + _files.addAll(files); + } + _archiveType = archiveTypeFromPath(assetPath, MFArchive.Type.ZIP); + _assetPath = assetPath.toLowerCase().endsWith(_archiveType.extension) ? assetPath + : (assetPath + "." + _archiveType.extension); + _compressionLevel = compressionLevel; + _analyze = analyze; + } - // TODO Auto-generated constructor stub + private static MFArchive.Type archiveTypeFromPath(String filePath, MFArchive.Type defaultType) { + if (filePath != null) { + String lcFileName = PathUtils.getFileName(filePath).toLowerCase(); + if (lcFileName.endsWith(".tar.gz") || lcFileName.endsWith(".gtar") || lcFileName.endsWith(".tgz")) { + return MFArchive.Type.GZIPPED_TAR; + } else if (lcFileName.endsWith(".zip")) { + return MFArchive.Type.ZIP; + } else if (lcFileName.endsWith(".tar")) { + return MFArchive.Type.TAR; + } else if (lcFileName.endsWith(".jar")) { + return MFArchive.Type.JAR; + } else if (lcFileName.endsWith(".aar")) { + return MFArchive.Type.AAR; + } + } + return defaultType; } @Override public void execute() throws Throwable { - // if (_files == null || _files.isEmpty()) { - // return; - // } - // String store = AssetNamespaceUtils.getStore(session(), _dstAssetNS); - // String source = _files.size() == 1 ? _files.get(0).path() : null; - // Archive.declareSupportForAllTypes(); - // ServerClient.Input input = new ServerClient.GeneratedInput(_arcType.mimeType, _arcType.extension, source, - // _compressLevel, store) { + Archive.declareSupportForAllTypes(); + Path baseDir = _files.size() == 1 ? _files.iterator().next().getParent() + : PathUtils.getLongestCommonParent(_files); + String source = _files.size() == 1 ? _files.iterator().next().toString() : null; + String store = getAssetStoreFor(session(), _assetPath); + store = store == null ? null : ("asset:" + store); + ServerClient.Input input = new MFGeneratedInput(_archiveType.mimeType, _archiveType.extension, source, -1, + store) { + + @Override + protected void consume(OutputStream out, AbortCheck ac) throws Throwable { + ArchiveOutput ao = ArchiveRegistry.createOutput(out, _archiveType.mimeType, _compressionLevel, null); + try { + for (Path f : _files) { + add(ao, f, baseDir); + } + } finally { + ao.close(); + } + } + }; + + XmlStringWriter w = new XmlStringWriter(); + w.add("id", "path=" + _assetPath); + w.add("create", true); + w.add("analyze", _analyze); + session().execute("asset.set", w.document(), input); + String assetId = AssetUtils.getAssetMetaByPath(session(), _assetPath).value("@id"); + System.out.println("Imported Mediaflux asset(id=" + assetId + "): " + _assetPath); + } + + private void add(ArchiveOutput ao, Path f, Path baseDir) throws Throwable { + if (!Files.isDirectory(f)) { + addFile(ao, f, baseDir); + } else { + addDirectory(ao, f, baseDir); + } + } + + private void addFile(ArchiveOutput ao, Path f, Path baseDir) throws Throwable { + String name; + if (baseDir == null) { + name = f.getFileName().toString(); + } else { + String filePath = f.toString(); + String baseDirPath = baseDir.toString(); + if (filePath.startsWith(baseDirPath)) { + name = filePath.substring(baseDirPath.length() + 1); + } else { + baseDir = PathUtils.getLongestCommonParent(f, baseDir); + baseDirPath = baseDir == null ? null : baseDir.toString(); + name = baseDir == null ? filePath : filePath.substring(baseDirPath.length() + 1); + } + } + logger().info("Adding '" + f + "'"); + ao.add(null, name, f.toFile()); + } + + private void addDirectory(ArchiveOutput ao, Path dir, Path baseDir) throws Throwable { + Files.walkFileTree(dir, EnumSet.noneOf(FileVisitOption.class), Integer.MAX_VALUE, + new SimpleFileVisitor<Path>() { + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + try { + addFile(ao, file, baseDir); + } catch (Throwable e) { + 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 - // protected void copyTo(OutputStream arg0, AbortCheck arg1) throws Throwable { - // // TODO Auto-generated method stub + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException ioe) { + if (ioe != null) { + logger().log(Level.SEVERE, "[postVisitDirectory] " + dir + ": " + ioe.getMessage(), ioe); + } + return FileVisitResult.CONTINUE; + } - // } - // }; + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return super.preVisitDirectory(dir, attrs); + } + }); } + private static String getAssetStoreFor(MFSession session, String assetPath) throws Throwable { + String closestExistingAssetNamespace = getClosestExistingAssetNamespace(session, assetPath); + if (closestExistingAssetNamespace != null) { + String store = AssetNamespaceUtils.getStore(session, closestExistingAssetNamespace); + return store; + } + return null; + } + + private static String getClosestExistingAssetNamespace(MFSession session, String assetPath) throws Throwable { + if (assetPath != null) { + Path path = Paths.get(assetPath.startsWith("/") ? assetPath : ("/" + assetPath)); + Path parentPath = path.getParent(); + while (parentPath != null) { + boolean exists = AssetNamespaceUtils.assetNamespaceExists(session, parentPath.toString()); + if (exists) { + return parentPath.toString(); + } else { + parentPath = parentPath.getParent(); + } + } + } + return null; + } } diff --git a/src/main/java/unimelb/utils/PathUtils.java b/src/main/java/unimelb/utils/PathUtils.java index 578a3889770eb324dac4d0335f7ee8b629189123..6dd147db394b73411bfc055660e3c1066e22a2a1 100644 --- a/src/main/java/unimelb/utils/PathUtils.java +++ b/src/main/java/unimelb/utils/PathUtils.java @@ -3,6 +3,8 @@ package unimelb.utils; import java.io.File; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collection; +import java.util.Iterator; public class PathUtils { @@ -271,6 +273,70 @@ public class PathUtils { return false; } + public static Path getLongestCommonParent(Path... files) { + if (files == null || files.length < 2) { + return null; + } + Path commonParent = null; + /** + * compare the first two files... + */ + if (files[0] != null && files[1] != null) { + Path rp = files[0].relativize(files[1]).normalize(); + while (rp != null && !rp.endsWith("..")) { + rp = rp.getParent(); + } + if (rp != null) { + commonParent = files[0].resolve(rp).normalize(); + commonParent = (commonParent.toString() == null || commonParent.toString().isBlank()) ? null + : commonParent; + } + } + + // return if there're only two files + if (files.length == 2) { + return commonParent; + } + // compare the rest files + for (int i = 2; i < files.length; i += 1) { + commonParent = getLongestCommonParent(files[i], commonParent); + } + return commonParent; + } + + public static Path getLongestCommonParent(Collection<Path> files) { + if (files == null || files.isEmpty()) { + return null; + } + + Iterator<Path> iterator = files.iterator(); + Path file1 = iterator.next(); + if (!iterator.hasNext()) { + return null; + } + Path file2 = iterator.next(); + + Path commonParent = null; + // get common parent of the first two files.. + if (file1 != null && file2 != null) { + Path rp = file1.relativize(file2).normalize(); + while (rp != null && !rp.endsWith("..")) { + rp = rp.getParent(); + } + if (rp != null) { + commonParent = file1.resolve(rp).normalize(); + commonParent = (commonParent.toString() == null || commonParent.toString().isBlank()) ? null + : commonParent; + } + } + + // compare the rest files + while (iterator.hasNext()) { + commonParent = getLongestCommonParent(iterator.next(), commonParent); + } + return commonParent; + } + public static void main(String[] args) { // System.out.println(joinSystemIndependent("ab/c", "\\d\\\\e\\", // "eee//ddd")); diff --git a/src/main/scripts/unix/unimelb-mf-import-archive b/src/main/scripts/unix/unimelb-mf-import-archive new file mode 100644 index 0000000000000000000000000000000000000000..9a011bad744e72e6a1e71097e96cd71cd89dcf32 --- /dev/null +++ b/src/main/scripts/unix/unimelb-mf-import-archive @@ -0,0 +1,28 @@ +#!/bin/bash + +# ${ROOT}/bin/ +BIN=$(dirname ${BASH_SOURCE[0]}) + +# current directory +CWD=$(pwd) + +# ${ROOT}/ +ROOT=$(cd ${BIN}/../../ && pwd && cd ${CWD}) + +# ${ROOT}/lib/ +LIB=${ROOT}/lib + +# ${ROOT}/lib/unimelb-mf-clients.jar +JAR=${LIB}/unimelb-mf-clients.jar + +# check if unimelb-mf-clients.jar exists +[[ ! -f $JAR ]] && echo "${JAR} is not found." >&2 && exit 2 + +#export JAVA_HOME=${ROOT}/@JAVA_HOME@ +#export PATH=${JAVA_HOME}/bin:${PATH} + +# check if java exists +[[ -z $(which java) ]] && echo "Java is not found." >&2 && exit 1 + +# execute the command +java -XX:+UseG1GC -XX:+UseStringDeduplication -Xmx1g -cp "${JAR}" unimelb.mf.client.sync.cli.MFImportArchive ${1+"$@"} diff --git a/src/main/scripts/windows/unimelb-mf-import-archive.cmd b/src/main/scripts/windows/unimelb-mf-import-archive.cmd new file mode 100644 index 0000000000000000000000000000000000000000..ecb93e4e4935bb899dbc9c3136b4b1cb494f3259 --- /dev/null +++ b/src/main/scripts/windows/unimelb-mf-import-archive.cmd @@ -0,0 +1,12 @@ +@echo off + +pushd %~dp0..\..\ +set ROOT=%cd% +popd + +@REM set JAVA_HOME=%ROOT%\@JAVA_HOME@ +@REM set PATH=%JAVA_HOME%\bin;%PATH% + +set JAR=%ROOT%\lib\unimelb-mf-clients.jar + +java -XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -Xmx1g -cp "%JAR%" unimelb.mf.client.sync.cli.MFImportArchive %*