Archive.IO implementation for BLOB files.
diff --git a/alien/src/wormarc/ArchiveManifest.java b/alien/src/wormarc/ArchiveManifest.java --- a/alien/src/wormarc/ArchiveManifest.java +++ b/alien/src/wormarc/ArchiveManifest.java @@ -169,7 +169,8 @@ public class ArchiveManifest { outputStream.writeInt(linkCount); } - // Placeholder for archive manifest topkey. + // ALWAYS write placeholder for archive manifest topkey. + // See shouldSkipOne below for the non-sentinel case. outputStream.write(LinkDigest.NULL_DIGEST.getBytes()); boolean shouldSkipOne = (!insertSentinel); @@ -187,8 +188,8 @@ public class ArchiveManifest { return new ByteArrayInputStream(buffer.toByteArray()); } - // Always closes stream. - public static ArchiveManifest fromBytes(InputStream rawBytes, LinkDigest chainHeadFixup) throws IOException { + public static ArchiveManifest fromBytes(InputStream rawBytes, LinkDigest chainHeadFixup, + boolean closeStream) throws IOException { try { if (chainHeadFixup == null || chainHeadFixup.isNullDigest()) { throw new IOException("chainHeadFixup is null or NULL_DIGEST!"); @@ -235,15 +236,21 @@ public class ArchiveManifest { } blocks.add(new Block(digests)); } - inputStream.close(); + if (closeStream) { + inputStream.close(); + } rawBytes = null; return new ArchiveManifest(rootObjects, blocks); } finally { - if (rawBytes != null) { + if (rawBytes != null && closeStream) { rawBytes.close(); } } } + public static ArchiveManifest fromBytes(InputStream rawBytes, LinkDigest chainHeadFixup) + throws IOException { + return fromBytes(rawBytes, chainHeadFixup, true); + } // Hmmmm... Move? public static LinkDigest getArchiveManifestDigest(List<Archive.RootObject> rootObjects) diff --git a/alien/src/wormarc/cli/CLI.java b/alien/src/wormarc/cli/CLI.java --- a/alien/src/wormarc/cli/CLI.java +++ b/alien/src/wormarc/cli/CLI.java @@ -46,6 +46,7 @@ import wormarc.LinkDigest; import wormarc.ExternalRefs; import wormarc.RootObjectKind; +import wormarc.io.FileIO; import wormarc.io.FreenetIO; import wormarc.io.FreenetTopKey; @@ -588,6 +589,46 @@ public class CLI { } }, + new Command("load", true, false, " <file_name>", + "Reads an archive version from a blob file.") { + public boolean canParse(String[] args) { return args.length == 2; } + public void invoke(String[] args, CLICache cache) throws Exception { + String fileName = args[1]; + FileIO io = new FileIO(); + io.setFile(fileName); + sOut.println(String.format("Reading: %s", fileName)); + Archive archive = Archive.load(io); + cache.setName("lastloaded.0"); + archive.write(cache); + cache.saveHead(cache.getName()); + sOut.println(String.format("Read and switched head to: %s", cache.getName())); + sOut.println(String.format("File metadata: %s", io.getMetaData())); + } + }, + + new Command("save", true, false, " <file_name>", + "Writes an archive version to a blob file.") { + public boolean canParse(String[] args) { return args.length >= 2; } + public void invoke(String[] args, CLICache cache) throws Exception { + Archive archive = null; + try { + archive = loadHead(cache); + } catch (IOException ioe) { + sOut.println("No head found!"); + return; + } + String fileName = args[1]; + FileIO io = new FileIO(); + io.setFile(fileName); + if (args.length > 2) { + sOut.println(String.format("Metadata: %s", args[2])); + io.setMetaData(args[2]); + } + sOut.println(String.format("Writing: %s", fileName)); + archive.write(io); + } + }, + // Debugging hack. Remove. new Command("showargs", false, false, " <arg list>", "print the args passed to it.") { public boolean canParse(String[] args) { return true; } diff --git a/alien/src/wormarc/io/BlobIO.java b/alien/src/wormarc/io/BlobIO.java new file mode 100644 --- /dev/null +++ b/alien/src/wormarc/io/BlobIO.java @@ -0,0 +1,219 @@ +/* An Archive.IO implementation to read/write an Archive to/from a BLOB. + * + * Copyright (C) 2010, 2011 Darrell Karbott + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks + * + * This file was developed as component of + * "fniki" (a wiki implementation running over Freenet). + */ + +package wormarc.io; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import wormarc.Archive; +import wormarc.ArchiveManifest; // Hmmm... +import wormarc.BinaryLinkRep; +import wormarc.Block; +import wormarc.HistoryLink; +import wormarc.HistoryLinkMap; +import wormarc.IOUtil; +import wormarc.LinkDataFactory; +import wormarc.LinkDigest; + +public class BlobIO implements Archive.IO { + protected final StreamFactory mStreamFactory; + protected String mVersion; + protected String mMetaData; + protected LinkDigest mFixup; + + // REQUIRES: Length must not change. + protected final static String VERSION_1 = "BLOB0001"; + + // Anything that you can read or write a BLOB to. + public interface StreamFactory { + InputStream getInputStream() throws IOException; + OutputStream getOutputStream() throws IOException; + boolean shouldCloseInputStream(); + boolean shouldCloseOutputStream(); + } + + protected void readHeader(DataInputStream inputStream) throws IOException { + byte[] versionBytes = new byte[8]; + inputStream.readFully(versionBytes); + mVersion = new String(versionBytes, IOUtil.UTF8); + if (!mVersion.equals(VERSION_1)) { + throw new IOException("Version mismatch or bad data."); + } + + int rawLength = inputStream.readUnsignedShort(); + mMetaData = ""; + if (rawLength == 0) { + return; + } + + byte[] raw = new byte[rawLength]; + inputStream.readFully(raw); + mMetaData = new String(raw, IOUtil.UTF8); + + } + + // Doesn't read the history links. + protected Archive.ArchiveData readArchiveData(DataInputStream inputStream) throws IOException { + byte[] rawFixup = new byte[20]; + inputStream.readFully(rawFixup); + return ArchiveManifest.fromBytes(inputStream, new LinkDigest(rawFixup), false).makeArchiveData(); + } + + protected void readLinks(HistoryLinkMap linkMap, LinkDataFactory linkFactory, + DataInputStream inputStream) throws IOException { + while (true) { + HistoryLink link = BinaryLinkRep.fromBytes(inputStream, linkFactory); + if (link == null) { + break; + } + linkMap.addLink(link); + } + } + + protected void writeHeader(DataOutputStream outputStream) throws IOException { + outputStream.write(VERSION_1.getBytes(IOUtil.UTF8)); + + byte[] raw = mMetaData.getBytes(IOUtil.UTF8); + if (raw.length > 65535) { + throw new IOException("Metadata is too big."); + } + + outputStream.writeShort(raw.length); // DCI: test? works for unsigned values right? + if (mMetaData.length() == 0) { + return; + } + outputStream.write(raw); + } + + protected void writeArchiveData(List<Block> blocks, List<Archive.RootObject> rootObjects, + DataOutputStream outputStream) + throws IOException { + + LinkDigest digest = ArchiveManifest.getArchiveManifestDigest(rootObjects); + if (digest.isNullDigest()) { + throw new IOException("Can't write an archive without a non-null ARCHIVE_MANIFEST root object."); + } + + // We are using ArchiveManifest to serialize the ArchiveData + // purely for expediency. There is some dodgy code + // in it which requires the chainHeadFixup, even when we know it. :-( + // + // HACK: We need to store this for ArchiveManifest.fromBytes(). + outputStream.write(digest.getBytes()); + + ArchiveManifest manifest = new ArchiveManifest(rootObjects, blocks); + outputStream.write(IOUtil.readAndClose(manifest.toBytes())); + } + + protected Set<LinkDigest> writeBlocks(HistoryLinkMap linkMap, List<Block> blocks, + DataOutputStream outputStream) throws IOException { + Set<LinkDigest> knownLinks = new HashSet<LinkDigest>(); + for (Block block : blocks) { + for (HistoryLink link : linkMap.getLinks(block)) { + if (knownLinks.contains(link)) { + continue; + } + BinaryLinkRep.write(outputStream, link); + knownLinks.add(link.mHash); + } + } + return knownLinks; + } + + public BlobIO(StreamFactory streamFactory, String metaData) { + if (streamFactory == null) { + throw new IllegalArgumentException("streamFactory is null"); + } + mStreamFactory = streamFactory; + + if (metaData == null) { + throw new IllegalArgumentException("metaData is null"); + } + mMetaData = metaData; + + } + + public String getMetaData() { return mMetaData; } + public void setMetaData(String value) { + if (value == null) { + throw new IllegalArgumentException(); + } + mMetaData = value; + } + + //////////////////////////////////////////////////////////// + // Archive.IO implementation. + //////////////////////////////////////////////////////////// + + public void write(HistoryLinkMap linkMap, List<Block> blocks, List<Archive.RootObject> rootObjects) + throws IOException { + DataOutputStream outputStream = new DataOutputStream(mStreamFactory.getOutputStream()); + try { + writeHeader(outputStream); + writeArchiveData(blocks, rootObjects, outputStream); + + Set<LinkDigest> knownLinks = writeBlocks(linkMap, blocks, outputStream); + for (Archive.RootObject obj : rootObjects) { + if (!knownLinks.contains(obj.mDigest)) { + throw new IOException("Root object not in blocks: " + obj.mDigest); + } + } + + } finally { + if (mStreamFactory.shouldCloseOutputStream()) { + outputStream.close(); + } + } + } + + public Archive.ArchiveData read(HistoryLinkMap linkMap, LinkDataFactory linkFactory) throws IOException { + DataInputStream inputStream = new DataInputStream(mStreamFactory.getInputStream()); + try { + readHeader(inputStream); + Archive.ArchiveData archiveData = readArchiveData(inputStream); + readLinks(linkMap, linkFactory, inputStream); + + for (Archive.RootObject obj : archiveData.mRootObjects) { + if (!linkMap.contains(obj.mDigest)) { + throw new IOException("Root object not read from input stream: " + obj.mDigest); + } + } + return archiveData; + } finally { + if (mStreamFactory.shouldCloseInputStream()) { + inputStream.close(); + } + } + } +} diff --git a/alien/src/wormarc/io/FileIO.java b/alien/src/wormarc/io/FileIO.java new file mode 100644 --- /dev/null +++ b/alien/src/wormarc/io/FileIO.java @@ -0,0 +1,67 @@ +/* An Archive.IO implementation for files. + * + * Copyright (C) 2010, 2011 Darrell Karbott + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks + * + * This file was developed as component of + * "fniki" (a wiki implementation running over Freenet). + */ + +package wormarc.io; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; + +public class FileIO extends BlobIO { + private File mFile; + + protected File getFile() throws IOException { + if (mFile == null) { + throw new IOException("File not set!"); + } + return mFile; + } + + private static class StreamFactoryImpl implements StreamFactory { + FileIO mFio; + public void setTarget(FileIO fio) { + mFio = fio; + } + public InputStream getInputStream() throws IOException { return new FileInputStream(mFio.getFile()); } + public OutputStream getOutputStream() throws IOException { return new FileOutputStream(mFio.getFile()); } + public boolean shouldCloseInputStream() { return true; } + public boolean shouldCloseOutputStream() { return true;} + } + + public FileIO() { + super(new StreamFactoryImpl(), ""); + ((StreamFactoryImpl)mStreamFactory).setTarget(this); + } + + public void setFile(File file) { + mFile = file; + } + + public void setFile(String fileName) { + mFile = new File(fileName); + } +} \ No newline at end of file