/*
* A CCNx file proxy program.
*
* Copyright (C) 2008, 2009 Palo Alto Research Center, Inc.
*
* This work is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License version 2 as published by the
* Free Software Foundation.
* This work 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 program; if not, write to the
* Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
package org.ccnx.ccn.apps.ccnfileproxy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import org.ccnx.ccn.CCNFilterListener;
import org.ccnx.ccn.CCNHandle;
import org.ccnx.ccn.config.ConfigurationException;
import org.ccnx.ccn.impl.support.Log;
import org.ccnx.ccn.io.CCNFileOutputStream;
import org.ccnx.ccn.profiles.CommandMarker;
import org.ccnx.ccn.profiles.SegmentationProfile;
import org.ccnx.ccn.profiles.VersioningProfile;
import org.ccnx.ccn.profiles.metadata.MetadataProfile;
import org.ccnx.ccn.profiles.nameenum.NameEnumerationResponse;
import org.ccnx.ccn.profiles.nameenum.NameEnumerationResponse.NameEnumerationResponseMessage;
import org.ccnx.ccn.profiles.nameenum.NameEnumerationResponse.NameEnumerationResponseMessage.NameEnumerationResponseMessageObject;
import org.ccnx.ccn.profiles.security.KeyProfile;
import org.ccnx.ccn.protocol.CCNTime;
import org.ccnx.ccn.protocol.ContentName;
import org.ccnx.ccn.protocol.Exclude;
import org.ccnx.ccn.protocol.ExcludeComponent;
import org.ccnx.ccn.protocol.Interest;
import org.ccnx.ccn.protocol.MalformedContentNameStringException;
/**
* CCNFileProxy is a file system proxy that makes files on the local system
* available over the CCNx network. It takes a directory from which to serve files,
* which it treats as the root of its content tree, and an optional ccnx URI
* to serve as the prefix for that file content as represented in CCNx.
*
* For example, if you have a directory /foo in the file system, with the following
* contents:
* /foo/
* bar.txt
* baz/
* box.txt
* and you call CCNFileProxy /foo ccnx:/testprefix
*
* then asking for ccnx:/testprefix/bar.txt would return the file bar.txt (segmented
* appropriately), and asking for ccnx:/testprefix/baz/box.txt would return box.txt.
* The version for each file is set using the last modified information available from
* the file system for the real file (but the file is re-signed every time you ask
* for it from this server, so will result in slightly different pieces of content
* with different signatures). The default prefix is ccnx:/, which means asking
* for ccnx:/bar.txt would get you bar.txt.
*
* Future improvements:
* - cache the original signing information so even if the
* data falls out of ccnd's cache, you get the same signature information back,
* - implement a NE responder to list files.
* - signal handling
* - logging level control from a command line argument
* - move file writer to a separate thread
*/
public class CCNFileProxy implements CCNFilterListener {
static String DEFAULT_URI = "ccnx:/";
static int BUF_SIZE = 4096;
protected boolean _finished = false;
protected ContentName _prefix;
protected String _filePrefix;
protected File _rootDirectory;
protected CCNHandle _handle;
private ContentName _responseName = null;
public static void usage() {
System.err.println("usage: CCNFileProxy <file path to serve> [<ccn prefix URI> default: ccn:/]");
}
public CCNFileProxy(String filePrefix, String ccnxURI) throws MalformedContentNameStringException, ConfigurationException, IOException {
_prefix = ContentName.fromURI(ccnxURI);
_filePrefix = filePrefix;
_rootDirectory = new File(filePrefix);
if (!_rootDirectory.exists()) {
Log.severe("Cannot serve files from directory {0}: directory does not exist!", filePrefix);
throw new IOException("Cannot serve files from directory " + filePrefix + ": directory does not exist!");
}
_handle = CCNHandle.open();
//set response name for NE requests
_responseName = KeyProfile.keyName(null, _handle.keyManager().getDefaultKeyID());
}
public void start() throws IOException{
Log.info("Starting file proxy for " + _filePrefix + " on CCNx namespace " + _prefix + "...");
System.out.println("Starting file proxy for " + _filePrefix + " on CCNx namespace " + _prefix + "...");
// All we have to do is say that we're listening on our main prefix.
_handle.registerFilter(_prefix, this);
}
public boolean handleInterest(Interest interest) {
// Alright, we've gotten an interest. Either it's an interest for a stream we're
// already reading, or it's a request for a new stream.
Log.info("CCNFileProxy main responder: got new interest: {0}", interest);
// Test to see if we need to respond to it.
if (!_prefix.isPrefixOf(interest.name())) {
Log.info("Unexpected: got an interest not matching our prefix (which is {0})", _prefix);
return false;
}
// We see interests for all our segments, and the header. We want to only
// handle interests for the first segment of a file, and not the first segment
// of the header. Order tests so most common one (segments other than first, non-header)
// fails first.
if (SegmentationProfile.isSegment(interest.name()) && !SegmentationProfile.isFirstSegment(interest.name())) {
Log.info("Got an interest for something other than a first segment, ignoring {0}.", interest.name());
return false;
} else if (interest.name().contains(CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION.getBytes())) {
try {
Log.info("Got a name enumeration request: {0}", interest);
return nameEnumeratorResponse(interest);
} catch (IOException e) {
Log.warning("IOException generating name enumeration response to {0}: {1}: {2}", interest.name(), e.getClass().getName(), e.getMessage());
return false;
}
} else if (MetadataProfile.isHeader(interest.name())) {
Log.info("Got an interest for the first segment of the header, ignoring {0}.", interest.name());
return false;
}
// Write the file
try {
return writeFile(interest);
} catch (IOException e) {
Log.warning("IOException writing file {0}: {1}: {2}", interest.name(), e.getClass().getName(), e.getMessage());
return false;
}
}
protected File ccnNameToFilePath(ContentName name) {
ContentName fileNamePostfix = name.postfix(_prefix);
if (null == fileNamePostfix) {
// Only happens if interest.name() is not a prefix of _prefix.
Log.info("Unexpected: got an interest not matching our prefix (which is {0})", _prefix);
return null;
}
File fileToWrite = new File(_rootDirectory, fileNamePostfix.toString());
Log.info("file postfix {0}, resulting path name {1}", fileNamePostfix, fileToWrite.getAbsolutePath());
return fileToWrite;
}
/**
* Actually write the file; should probably run in a separate thread.
* @param fileNamePostfix
* @throws IOException
*/
protected boolean writeFile(Interest outstandingInterest) throws IOException {
File fileToWrite = ccnNameToFilePath(outstandingInterest.name());
Log.info("CCNFileProxy: extracted request for file: " + fileToWrite.getAbsolutePath() + " exists? ", fileToWrite.exists());
if (!fileToWrite.exists()) {
Log.warning("File {0} does not exist. Ignoring request.", fileToWrite.getAbsoluteFile());
return false;
}
FileInputStream fis = null;
try {
fis = new FileInputStream(fileToWrite);
} catch (FileNotFoundException fnf) {
Log.warning("Unexpected: file we expected to exist doesn't exist: {0}!", fileToWrite.getAbsolutePath());
return false;
}
// Set the version of the CCN content to be the last modification time of the file.
CCNTime modificationTime = new CCNTime(fileToWrite.lastModified());
ContentName versionedName =
VersioningProfile.addVersion(new ContentName(_prefix,
outstandingInterest.name().postfix(_prefix).components()), modificationTime);
// CCNFileOutputStream will use the version on a name you hand it (or if the name
// is unversioned, it will version it).
CCNFileOutputStream ccnout = new CCNFileOutputStream(versionedName, _handle);
// We have an interest already, register it so we can write immediately.
ccnout.addOutstandingInterest(outstandingInterest);
byte [] buffer = new byte[BUF_SIZE];
int read = fis.read(buffer);
while (read >= 0) {
ccnout.write(buffer, 0, read);
read = fis.read(buffer);
}
fis.close();
ccnout.close(); // will flush
return true;
}
/**
* Handle name enumeration requests
*
* @param interest
* @throws IOException
* @returns true if interest is consumed
*/
public boolean nameEnumeratorResponse(Interest interest) throws IOException {
boolean result = false;
ContentName neRequestPrefix = interest.name().cut(CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION.getBytes());
File directoryToEnumerate = ccnNameToFilePath(neRequestPrefix);
if (!directoryToEnumerate.exists() || !directoryToEnumerate.isDirectory()) {
// nothing to enumerate
return result;
}
NameEnumerationResponse ner = new NameEnumerationResponse();
ner.setPrefix(new ContentName(neRequestPrefix, CommandMarker.COMMAND_MARKER_BASIC_ENUMERATION.getBytes()));
Log.info("Directory to enumerate: {0}, last modified {1}", directoryToEnumerate.getAbsolutePath(), new CCNTime(directoryToEnumerate.lastModified()));
// stat() the directory to see when it last changed -- will change whenever
// a file is added or removed, which is the only thing that will change the
// list we return.
ner.setTimestamp(new CCNTime(directoryToEnumerate.lastModified()));
// See if the resulting response is later than the previous one we released.
//now add the response id
ContentName prefixWithId = new ContentName(ner.getPrefix(), _responseName.components());
//now finish up with version and segment
ContentName potentialCollectionName = VersioningProfile.addVersion(prefixWithId, ner.getTimestamp());
//switch to add response id to name enumeration objects
//ContentName potentialCollectionName = VersioningProfile.addVersion(ner.getPrefix(), ner.getTimestamp());
potentialCollectionName = SegmentationProfile.segmentName(potentialCollectionName, SegmentationProfile.baseSegment());
//check if we should respond...
if (interest.matches(potentialCollectionName, null)) {
// We want to set the version of the NE response to the time of the
// last modified file in the directory. Unfortunately that requires us to
// stat() all the files whether we are going to respond or not.
String [] children = directoryToEnumerate.list();
if ((null != children) && (children.length > 0)) {
for (int i = 0; i < children.length; ++i) {
ner.add(children[i]);
}
NameEnumerationResponseMessage nem = ner.getNamesForResponse();
NameEnumerationResponseMessageObject neResponse = new NameEnumerationResponseMessageObject(prefixWithId, nem, _handle);
neResponse.save(ner.getTimestamp(), interest);
result = true;
Log.info("sending back name enumeration response {0}, timestamp (version) {1}.", ner.getPrefix(), ner.getTimestamp());
} else {
Log.info("no children available: we are not sending back a response to the name enumeration interest (interest = {0}); our response would have been {1}", interest, potentialCollectionName);
}
} else {
Log.info("we are not sending back a response to the name enumeration interest (interest = {0}); our response would have been {1}", interest, potentialCollectionName);
if (interest.exclude().size() > 1) {
Exclude.Element el = interest.exclude().value(1);
if ((null != el) && (el instanceof ExcludeComponent)) {
Log.info("previous version: {0}", VersioningProfile.getVersionComponentAsTimestamp(((ExcludeComponent)el).getBytes()));
}
}
}
return result;
}
/**
* Turn off everything.
* @throws IOException
*/
public void shutdown() throws IOException {
if (null != _handle) {
_handle.unregisterFilter(_prefix, this);
Log.info("Shutting down file proxy for " + _filePrefix + " on CCNx namespace " + _prefix + "...");
System.out.println("Shutting down file proxy for " + _filePrefix + " on CCNx namespace " + _prefix + "...");
}
_finished = true;
}
public boolean finished() { return _finished; }
/**
* @param args
*/
public static void main(String[] args) {
if (args.length < 1) {
usage();
return;
}
String filePrefix = args[0];
String ccnURI = (args.length > 1) ? args[1] : DEFAULT_URI;
try {
CCNFileProxy proxy = new CCNFileProxy(filePrefix, ccnURI);
// All we need to do now is wait until interrupted.
proxy.start();
while (!proxy.finished()) {
// we really want to wait until someone ^C's us.
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
// do nothing
}
}
} catch (Exception e) {
Log.warning("Exception in ccnFileProxy: type: " + e.getClass().getName() + ", message: "+ e.getMessage());
Log.warningStackTrace(e);
System.err.println("Exception in ccnFileProxy: type: " + e.getClass().getName() + ", message: "+ e.getMessage());
e.printStackTrace();
}
}
}