From f387cd34ec00ca0a66201cfd44373ab2008a0da8 Mon Sep 17 00:00:00 2001
From: Dan Goodliffe <dan@randomdan.homeip.net>
Date: Mon, 11 Jan 2016 20:43:42 +0000
Subject: WIP MythFS inodes

---
 mythfs/etc/icebox.config                           |  2 +-
 mythfs/service/Jamfile.jam                         |  7 +-
 mythfs/service/inodes/abstractDynamicDirectory.cpp | 22 ++++++
 mythfs/service/inodes/abstractDynamicDirectory.h   | 18 +++++
 mythfs/service/inodes/allDirectory.cpp             | 20 +++++
 mythfs/service/inodes/allDirectory.h               | 22 ++++++
 mythfs/service/inodes/byDateDirectory.h            | 16 ++++
 mythfs/service/inodes/byTitleDirectory.h           | 15 ++++
 mythfs/service/inodes/file.h                       | 15 ++++
 mythfs/service/inodes/groupingDirectory.h          | 17 +++++
 mythfs/service/inodes/node.cpp                     | 23 ++++++
 mythfs/service/inodes/node.h                       | 27 +++++++
 mythfs/service/inodes/staticDirectory.cpp          | 34 +++++++++
 mythfs/service/inodes/staticDirectory.h            | 19 +++++
 mythfs/service/inodes/symlink.cpp                  | 25 ++++++
 mythfs/service/inodes/symlink.h                    | 22 ++++++
 mythfs/service/main.cpp                            | 24 +++---
 mythfs/service/openDirectory.cpp                   | 22 ++++++
 mythfs/service/openDirectory.h                     | 20 +++++
 mythfs/service/recordingsVolume.cpp                | 36 +++++++--
 mythfs/service/recordingsVolume.h                  |  8 +-
 mythfs/service/util.h                              | 31 ++++++++
 mythfs/unittests/fixtures/schema-min.sql           | 89 ++++++++++++++++++++++
 mythfs/unittests/mockDefs.cpp                      |  9 ++-
 mythfs/unittests/mockDefs.h                        |  2 +-
 mythfs/unittests/testMain.cpp                      | 60 +++++++++++++++
 mythfs/util.h                                      | 33 ++++++++
 27 files changed, 612 insertions(+), 26 deletions(-)
 create mode 100644 mythfs/service/inodes/abstractDynamicDirectory.cpp
 create mode 100644 mythfs/service/inodes/abstractDynamicDirectory.h
 create mode 100644 mythfs/service/inodes/allDirectory.cpp
 create mode 100644 mythfs/service/inodes/allDirectory.h
 create mode 100644 mythfs/service/inodes/byDateDirectory.h
 create mode 100644 mythfs/service/inodes/byTitleDirectory.h
 create mode 100644 mythfs/service/inodes/file.h
 create mode 100644 mythfs/service/inodes/groupingDirectory.h
 create mode 100644 mythfs/service/inodes/node.cpp
 create mode 100644 mythfs/service/inodes/node.h
 create mode 100644 mythfs/service/inodes/staticDirectory.cpp
 create mode 100644 mythfs/service/inodes/staticDirectory.h
 create mode 100644 mythfs/service/inodes/symlink.cpp
 create mode 100644 mythfs/service/inodes/symlink.h
 create mode 100644 mythfs/service/openDirectory.cpp
 create mode 100644 mythfs/service/openDirectory.h
 create mode 100644 mythfs/service/util.h
 create mode 100644 mythfs/unittests/fixtures/schema-min.sql
 create mode 100644 mythfs/util.h

diff --git a/mythfs/etc/icebox.config b/mythfs/etc/icebox.config
index f7ee191..30fe04b 100644
--- a/mythfs/etc/icebox.config
+++ b/mythfs/etc/icebox.config
@@ -1 +1 @@
-IceBox.Service.MythFS=mythfs:createIceTrayService --MythFS.ThreadPool.Size=2 --MythFS.ThreadPool.SizeMax=10 --mythconverg.Database.ConnectionString="server=mysql user=mythtv database=mythconverg password=mythpass" --GentooBrowseAPI.Endpoints="tcp -p 4001"
+IceBox.Service.MythFS=mythfs:createIceTrayService --MythFS.ThreadPool.Size=2 --MythFS.ThreadPool.SizeMax=10 --MythFS.Database.ConnectionString="server=mysql;user=mythtv;database=mythconverg;password=mythpass" --MythFS.Endpoints="tcp -p 4001"
diff --git a/mythfs/service/Jamfile.jam b/mythfs/service/Jamfile.jam
index 7dc26cb..2a209e1 100644
--- a/mythfs/service/Jamfile.jam
+++ b/mythfs/service/Jamfile.jam
@@ -1,13 +1,15 @@
 import icetray ;
+import package ;
 
 lib mythfs :
-	[ glob *.cpp *.ice sql/*.sql ]
+	[ glob *.cpp inodes/*.cpp *.ice sql/*.sql ]
 	:
 	<slicer>yes
 	<library>..//netfsComms
 	<library>..//adhocutil
 	<library>..//dbppcore
 	<library>..//boost_system
+	<library>..//boost_filesystem
 	<library>..//boost_date_time
 	<library>..//Ice
 	<library>..//IceBox
@@ -18,9 +20,12 @@ lib mythfs :
 	<library>..//slicer-db
 	<library>../..//glibmm
 	<icetray.sql.namespace>MythFS
+	<include>.
 	: :
 	<include>.
 	<library>..//netfsComms
 	<library>..//icetray
 	;
 
+package.install install : : : mythfs ;
+
diff --git a/mythfs/service/inodes/abstractDynamicDirectory.cpp b/mythfs/service/inodes/abstractDynamicDirectory.cpp
new file mode 100644
index 0000000..0a703db
--- /dev/null
+++ b/mythfs/service/inodes/abstractDynamicDirectory.cpp
@@ -0,0 +1,22 @@
+#include "abstractDynamicDirectory.h"
+#include <sys/stat.h>
+#include <exceptions.h>
+
+namespace MythFS {
+	NetFS::Attr
+	AbstractDynamicDirectory::getattr() const
+	{
+		return {
+			0, 0,
+			S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH | S_IFDIR,
+			1, "root", "root", 0, 0, 0, 0, 0, 0, 0
+		};
+	}
+
+	Node::PointerType
+	AbstractDynamicDirectory::getChild(const std::string &) const
+	{
+		throw ::NetFS::SystemError(EINVAL);
+	}
+}
+
diff --git a/mythfs/service/inodes/abstractDynamicDirectory.h b/mythfs/service/inodes/abstractDynamicDirectory.h
new file mode 100644
index 0000000..0d62931
--- /dev/null
+++ b/mythfs/service/inodes/abstractDynamicDirectory.h
@@ -0,0 +1,18 @@
+#ifndef MYTHFS_ABSTRACTDYNAMICDIRECTORY_H
+#define MYTHFS_ABSTRACTDYNAMICDIRECTORY_H
+
+#include "node.h"
+
+namespace MythFS {
+	class AbstractDynamicDirectory : public Node {
+		public:
+			NetFS::Attr getattr() const override;
+
+		protected:
+			virtual PointerType getChild(const std::string &) const override = 0;
+			virtual NetFS::NameList getContents() const override = 0;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/allDirectory.cpp b/mythfs/service/inodes/allDirectory.cpp
new file mode 100644
index 0000000..d61b8b9
--- /dev/null
+++ b/mythfs/service/inodes/allDirectory.cpp
@@ -0,0 +1,20 @@
+#include "allDirectory.h"
+#include <util.h>
+#include "symlink.h"
+
+namespace MythFS {
+	AllDirectory::AllDirectory(const DBPrx & d) : db(d) { }
+
+	NetFS::NameList
+	AllDirectory::getContents() const
+	{
+		return db->getRecorded() / &Recorded::basename;
+	}
+
+	Node::PointerType
+	AllDirectory::getChild(const std::string & path) const
+	{
+		return new Symlink("/var/store/mythrecordings/" + path);
+	}
+}
+
diff --git a/mythfs/service/inodes/allDirectory.h b/mythfs/service/inodes/allDirectory.h
new file mode 100644
index 0000000..67165ef
--- /dev/null
+++ b/mythfs/service/inodes/allDirectory.h
@@ -0,0 +1,22 @@
+#ifndef MYTHFS_ALLDIRECTORY_H
+#define MYTHFS_ALLDIRECTORY_H
+
+#include "abstractDynamicDirectory.h"
+#include "myth-db.h"
+
+namespace MythFS {
+	class AllDirectory : public AbstractDynamicDirectory {
+		public:
+			AllDirectory(const DBPrx &);
+
+		protected:
+			NetFS::NameList getContents() const override;
+			PointerType getChild(const std::string &) const override;
+
+		private:
+			DBPrx db;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/byDateDirectory.h b/mythfs/service/inodes/byDateDirectory.h
new file mode 100644
index 0000000..945a9d0
--- /dev/null
+++ b/mythfs/service/inodes/byDateDirectory.h
@@ -0,0 +1,16 @@
+#ifndef MYTHFS_BYTITLEDIRECTORY_H
+#define MYTHFS_BYTITLEDIRECTORY_H
+
+#include "groupingDirectory.h"
+
+namespace MythFS {
+	class ByDateDirectory : public GroupingDirectory<std::string> {
+
+		protected:
+			virtual std::string attribute(const MythFS::RecordedPtr &) const;
+	};
+}
+
+#endif
+
+
diff --git a/mythfs/service/inodes/byTitleDirectory.h b/mythfs/service/inodes/byTitleDirectory.h
new file mode 100644
index 0000000..e9883d1
--- /dev/null
+++ b/mythfs/service/inodes/byTitleDirectory.h
@@ -0,0 +1,15 @@
+#ifndef MYTHFS_BYTITLEDIRECTORY_H
+#define MYTHFS_BYTITLEDIRECTORY_H
+
+#include "groupingDirectory.h"
+
+namespace MythFS {
+	class ByTitleDirectory : public GroupingDirectory<std::string> {
+
+		protected:
+			virtual std::string attribute(const MythFS::RecordedPtr &) const;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/file.h b/mythfs/service/inodes/file.h
new file mode 100644
index 0000000..e71a343
--- /dev/null
+++ b/mythfs/service/inodes/file.h
@@ -0,0 +1,15 @@
+#ifndef MYTHFS_FILE_H
+#define MYTHFS_FILE_H
+
+#include "node.h"
+
+namespace MythFS {
+	class File : public Node {
+		public:
+			NetFS::Attr getattr() const override;
+			PointerType getChild(const std::string &) const override;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/groupingDirectory.h b/mythfs/service/inodes/groupingDirectory.h
new file mode 100644
index 0000000..82b3c0c
--- /dev/null
+++ b/mythfs/service/inodes/groupingDirectory.h
@@ -0,0 +1,17 @@
+#ifndef MYTHFS_GROUPINGDIRECTORY_H
+#define MYTHFS_GROUPINGDIRECTORY_H
+
+#include "abstractDynamicDirectory.h"
+
+namespace MythFS {
+	template <typename T>
+	class GroupingDirectory : public AbstractDynamicDirectory {
+		public:
+		protected:
+			virtual T attribute(const MythFS::RecordedPtr &) const = 0;
+			NetFS::NameList getContents() const override;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/node.cpp b/mythfs/service/inodes/node.cpp
new file mode 100644
index 0000000..19e4fd3
--- /dev/null
+++ b/mythfs/service/inodes/node.cpp
@@ -0,0 +1,23 @@
+#include "node.h"
+#include <exceptions.h>
+
+namespace MythFS {
+	Node::PointerType
+	Node::getChild(const std::string &) const
+	{
+		throw ::NetFS::SystemError(ENOTDIR);
+	}
+
+	std::string
+	Node::readlink() const
+	{
+		throw ::NetFS::SystemError(EINVAL);
+	}
+
+	NetFS::NameList
+	Node::getContents() const
+	{
+		throw NetFS::SystemError(ENOTDIR);
+	}
+}
+
diff --git a/mythfs/service/inodes/node.h b/mythfs/service/inodes/node.h
new file mode 100644
index 0000000..b88e403
--- /dev/null
+++ b/mythfs/service/inodes/node.h
@@ -0,0 +1,27 @@
+#ifndef MYTHFS_NODE_H
+#define MYTHFS_NODE_H
+
+#include <map>
+#include <string>
+#include <IceUtil/Handle.h>
+#include <Ice/Object.h>
+#include <types.h>
+
+namespace MythFS {
+	class Node;
+
+	typedef std::map<std::string, IceUtil::Handle<Node>> Contents;
+
+	class Node : public virtual Ice::Object {
+		public:
+			typedef IceUtil::Handle<Node> PointerType;
+
+			virtual NetFS::Attr getattr() const = 0;
+			virtual std::string readlink() const;
+			virtual PointerType getChild(const std::string &) const;
+			virtual NetFS::NameList getContents() const;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/staticDirectory.cpp b/mythfs/service/inodes/staticDirectory.cpp
new file mode 100644
index 0000000..31cc4fa
--- /dev/null
+++ b/mythfs/service/inodes/staticDirectory.cpp
@@ -0,0 +1,34 @@
+#include "staticDirectory.h"
+#include <sys/stat.h>
+#include <safeMapFind.h>
+#include <exceptions.h>
+#include <util.h>
+
+namespace MythFS {
+	NetFS::Attr
+	StaticDirectory::getattr() const
+	{
+		return {
+			0, 0,
+			S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH | S_IFDIR,
+			1, "root", "root", 0, 0, 0, 0, 0, 0, 0};
+	}
+
+	class NoSuchFileOrDirectory : public ::NetFS::SystemError {
+		public:
+			NoSuchFileOrDirectory(const std::string &) : ::NetFS::SystemError(ENOENT) {}
+	};
+
+	Node::PointerType
+	StaticDirectory::getChild(const std::string & path) const
+	{
+		return AdHoc::safeMapLookup<NoSuchFileOrDirectory>(contents, path);
+	}
+
+	NetFS::NameList
+	StaticDirectory::getContents() const
+	{
+		return contents / &Contents::value_type::first;
+	}
+}
+
diff --git a/mythfs/service/inodes/staticDirectory.h b/mythfs/service/inodes/staticDirectory.h
new file mode 100644
index 0000000..0ebb585
--- /dev/null
+++ b/mythfs/service/inodes/staticDirectory.h
@@ -0,0 +1,19 @@
+#ifndef MYTHFS_STATICDIRECTORY_H
+#define MYTHFS_STATICDIRECTORY_H
+
+#include "node.h"
+
+namespace MythFS {
+	class StaticDirectory : public Node {
+		public:
+			NetFS::Attr getattr() const override;
+			PointerType getChild(const std::string &) const override;
+			NetFS::NameList getContents() const override;
+
+		protected:
+			Contents contents;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/inodes/symlink.cpp b/mythfs/service/inodes/symlink.cpp
new file mode 100644
index 0000000..8dcc0df
--- /dev/null
+++ b/mythfs/service/inodes/symlink.cpp
@@ -0,0 +1,25 @@
+#include "symlink.h"
+#include <sys/stat.h>
+
+namespace MythFS {
+	Symlink::Symlink(const std::string & t) :
+		target(t)
+	{
+
+	}
+
+	NetFS::Attr
+	Symlink::getattr() const
+	{
+		return {
+			0, 0,
+			S_IRUSR | S_IRGRP | S_IROTH | S_IFLNK,
+			1,"root","root",0,0,0,0,0,0,0};
+	}
+
+	std::string
+	Symlink::readlink() const
+	{
+		return target;
+	}
+}
diff --git a/mythfs/service/inodes/symlink.h b/mythfs/service/inodes/symlink.h
new file mode 100644
index 0000000..227d2bd
--- /dev/null
+++ b/mythfs/service/inodes/symlink.h
@@ -0,0 +1,22 @@
+#ifndef MYTHFS_SYMLINK_H
+#define MYTHFS_SYMLINK_H
+
+#include <types.h>
+#include "node.h"
+
+namespace MythFS {
+	class Symlink : public Node {
+		public:
+			Symlink(const std::string &);
+
+			NetFS::Attr getattr() const override;
+			std::string readlink() const override;
+
+		private:
+			const std::string target;
+
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/main.cpp b/mythfs/service/main.cpp
index 145cf71..6aafc7a 100644
--- a/mythfs/service/main.cpp
+++ b/mythfs/service/main.cpp
@@ -5,16 +5,18 @@
 #include "recordingsVolume.h"
 #include "dbimpl.h"
 
-class Api : public IceTray::Service {
-	public:
-		void addObjects(const std::string &, const Ice::CommunicatorPtr & ic, const Ice::StringSeq &, const Ice::ObjectAdapterPtr & adp) override
-		{
-			auto db = getConnectionPool(ic, "mysql", "mythconverg");
-			adp->add(new MythFS::Service(), ic->stringToIdentity("service"));
-			adp->add(new MythFS::DBImpl(db), ic->stringToIdentity("db"));
-			adp->add(new MythFS::RecordingsVolume(), ic->stringToIdentity("recordings"));
-		}
-};
+namespace MythFS {
+	class Api : public IceTray::Service {
+		public:
+			void addObjects(const std::string &, const Ice::CommunicatorPtr & ic, const Ice::StringSeq &, const Ice::ObjectAdapterPtr & adp) override
+			{
+				auto dbpool = getConnectionPool(ic, "mysql", "MythFS");
+				adp->add(new MythFS::Service(), ic->stringToIdentity("Service"));
+				auto dbservice = DBPrx::uncheckedCast(adp->add(new DBImpl(dbpool), ic->stringToIdentity("DB")));
+				adp->add(new RecordingsVolume(dbservice), ic->stringToIdentity("recordings"));
+			}
+	};
 
-NAMEDFACTORY("default", Api, IceTray::ServiceFactory);
+	NAMEDFACTORY("default", MythFS::Api, IceTray::ServiceFactory);
+}
 
diff --git a/mythfs/service/openDirectory.cpp b/mythfs/service/openDirectory.cpp
new file mode 100644
index 0000000..42a3e59
--- /dev/null
+++ b/mythfs/service/openDirectory.cpp
@@ -0,0 +1,22 @@
+#include "openDirectory.h"
+#include <Ice/ObjectAdapter.h>
+
+namespace MythFS {
+	OpenDirectory::OpenDirectory(Node::PointerType d) :
+		directory(d)
+	{
+	}
+
+	::NetFS::NameList
+	OpenDirectory::readdir(const Ice::Current &)
+	{
+		return directory->getContents();
+	}
+
+	void
+	OpenDirectory::close(const Ice::Current & ic)
+	{
+		ic.adapter->remove(ic.id);
+	}
+}
+
diff --git a/mythfs/service/openDirectory.h b/mythfs/service/openDirectory.h
new file mode 100644
index 0000000..40215f5
--- /dev/null
+++ b/mythfs/service/openDirectory.h
@@ -0,0 +1,20 @@
+#ifndef MYTHFS_OPENDIRECTORY_H
+#define MYTHFS_OPENDIRECTORY_H
+
+#include <directory.h>
+#include "inodes/node.h"
+
+namespace MythFS {
+	class OpenDirectory : public ::NetFS::Directory {
+		public:
+			OpenDirectory(Node::PointerType);
+
+			::NetFS::NameList readdir(const Ice::Current &) override;
+			void close(const Ice::Current &) override;
+		private:
+			Node::PointerType directory;
+	};
+}
+
+#endif
+
diff --git a/mythfs/service/recordingsVolume.cpp b/mythfs/service/recordingsVolume.cpp
index cfebe46..f5bf07b 100644
--- a/mythfs/service/recordingsVolume.cpp
+++ b/mythfs/service/recordingsVolume.cpp
@@ -1,10 +1,21 @@
 #include "recordingsVolume.h"
+#include <myth-models.h>
+#include <boost/filesystem/path.hpp>
+
+#include <Ice/ObjectAdapter.h>
+#include "inodes/allDirectory.h"
+#include "openDirectory.h"
 
 namespace MythFS {
+	RecordingsVolume::RecordingsVolume(DBPrx db)
+	{
+		contents.insert({ "all", new AllDirectory(db) });
+	}
+
 	NetFS::DirectoryPrx
-	RecordingsVolume::opendir(const NetFS::ReqEnv &, const std::string &, const Ice::Current&)
+	RecordingsVolume::opendir(const NetFS::ReqEnv &, const std::string & p, const Ice::Current & ic)
 	{
-		return NULL;
+		return ::NetFS::DirectoryPrx::uncheckedCast(ic.adapter->addWithUUID(new OpenDirectory(resolvePath(p))));
 	}
 	void RecordingsVolume::mkdir(const NetFS::ReqEnv &, const std::string &, Ice::Int, const Ice::Current&)
 	{
@@ -48,12 +59,12 @@ namespace MythFS {
 
 	Ice::Int RecordingsVolume::access(const NetFS::ReqEnv &, const std::string &, Ice::Int, const Ice::Current&)
 	{
-		throw ::NetFS::SystemError(ENOSYS);
+		return 0;
 	}
 
-	NetFS::Attr RecordingsVolume::getattr(const NetFS::ReqEnv &, const std::string &, const Ice::Current&)
+	NetFS::Attr RecordingsVolume::getattr(const NetFS::ReqEnv &, const std::string & path, const Ice::Current&)
 	{
-		throw ::NetFS::SystemError(ENOSYS);
+		return resolvePath(path)->getattr();
 	}
 
 	void RecordingsVolume::mknod(const NetFS::ReqEnv &, const std::string &, Ice::Int, Ice::Int, const Ice::Current&)
@@ -76,9 +87,9 @@ namespace MythFS {
 		throw ::NetFS::SystemError(ENOSYS);
 	}
 
-	std::string RecordingsVolume::readlink(const NetFS::ReqEnv &, const std::string &, const Ice::Current&)
+	std::string RecordingsVolume::readlink(const NetFS::ReqEnv &, const std::string & path, const Ice::Current&)
 	{
-		throw ::NetFS::SystemError(ENOSYS);
+		return resolvePath(path)->readlink();
 	}
 
 	void RecordingsVolume::chmod(const NetFS::ReqEnv &, const std::string &, Ice::Int, const Ice::Current&)
@@ -99,5 +110,16 @@ namespace MythFS {
 	void RecordingsVolume::disconnect(const Ice::Current&)
 	{
 	}
+
+	Node::PointerType
+	RecordingsVolume::resolvePath(const std::string & p)
+	{
+		Node::PointerType d = this;
+		boost::filesystem::path path(p.substr(1));
+		for (const auto & e : path) {
+			d = d->getChild(e.string());
+		}
+		return d;
+	}
 }
 
diff --git a/mythfs/service/recordingsVolume.h b/mythfs/service/recordingsVolume.h
index 65357ca..4bf227d 100644
--- a/mythfs/service/recordingsVolume.h
+++ b/mythfs/service/recordingsVolume.h
@@ -2,10 +2,14 @@
 #define MYTHFS_RECORDINGS_VOLUME_H
 
 #include <volume.h>
+#include "inodes/staticDirectory.h"
+#include <myth-db.h>
 
 namespace MythFS {
-	class RecordingsVolume : public ::NetFS::Volume {
+	class RecordingsVolume : public ::NetFS::Volume, public StaticDirectory {
 		public:
+			RecordingsVolume(DBPrx);
+
 			virtual NetFS::DirectoryPrx opendir(const NetFS::ReqEnv &, const std::string & path, const Ice::Current&) override;
 
 			virtual void mkdir(const NetFS::ReqEnv &, const std::string & path, Ice::Int id, const Ice::Current&) override;
@@ -34,6 +38,8 @@ namespace MythFS {
 
 			virtual void disconnect(const Ice::Current&) override;
 
+		private:
+			Node::PointerType resolvePath(const std::string &);
 	};
 }
 
diff --git a/mythfs/service/util.h b/mythfs/service/util.h
new file mode 100644
index 0000000..5e42b34
--- /dev/null
+++ b/mythfs/service/util.h
@@ -0,0 +1,31 @@
+#ifndef MYTHFS_UTIL_H
+#define MYTHFS_UTIL_H
+
+#include <vector>
+
+template <typename T, typename MT>
+std::vector<typename std::remove_const<MT>::type>
+operator/(const T & input, MT T::value_type::element_type::* m)
+{
+	std::vector<typename std::remove_const<MT>::type> result;
+	result.reserve(input.size());
+	for (const auto & i : input) {
+		result.push_back(i.get()->*m);
+	}
+	return result;
+}
+
+template <typename T, typename MT>
+std::vector<typename std::remove_const<MT>::type>
+operator/(const T & input, MT T::value_type::* m)
+{
+	std::vector<typename std::remove_const<MT>::type> result;
+	result.reserve(input.size());
+	for (const auto & i : input) {
+		result.push_back(i.*m);
+	}
+	return result;
+}
+
+#endif
+
diff --git a/mythfs/unittests/fixtures/schema-min.sql b/mythfs/unittests/fixtures/schema-min.sql
new file mode 100644
index 0000000..ebf90d9
--- /dev/null
+++ b/mythfs/unittests/fixtures/schema-min.sql
@@ -0,0 +1,89 @@
+-- MySQL dump 10.13  Distrib 5.6.28, for Linux (x86_64)
+--
+-- Host: defiant    Database: mythconverg
+-- ------------------------------------------------------
+-- Server version	5.6.27-log
+
+/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
+/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
+/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
+/*!40101 SET NAMES utf8 */;
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+
+--
+-- Table structure for table `recorded`
+--
+
+/*!40101 SET @saved_cs_client     = @@character_set_client */;
+/*!40101 SET character_set_client = utf8 */;
+CREATE TABLE `recorded` (
+  `chanid` int(10) unsigned NOT NULL DEFAULT '0',
+  `starttime` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `endtime` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `title` varchar(128) NOT NULL DEFAULT '',
+  `subtitle` varchar(128) NOT NULL DEFAULT '',
+  `description` varchar(16000) NOT NULL DEFAULT '',
+  `season` smallint(5) NOT NULL,
+  `episode` smallint(5) NOT NULL,
+  `category` varchar(64) NOT NULL DEFAULT '',
+  `hostname` varchar(64) NOT NULL DEFAULT '',
+  `bookmark` tinyint(1) NOT NULL DEFAULT '0',
+  `editing` int(10) unsigned NOT NULL DEFAULT '0',
+  `cutlist` tinyint(1) NOT NULL DEFAULT '0',
+  `autoexpire` int(11) NOT NULL DEFAULT '0',
+  `commflagged` int(10) unsigned NOT NULL DEFAULT '0',
+  `recgroup` varchar(32) NOT NULL DEFAULT 'Default',
+  `recordid` int(11) DEFAULT NULL,
+  `seriesid` varchar(64) DEFAULT NULL,
+  `programid` varchar(64) DEFAULT NULL,
+  `inetref` varchar(40) NOT NULL,
+  `lastmodified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  `filesize` bigint(20) NOT NULL DEFAULT '0',
+  `stars` float NOT NULL DEFAULT '0',
+  `previouslyshown` tinyint(1) DEFAULT '0',
+  `originalairdate` date DEFAULT NULL,
+  `preserve` tinyint(1) NOT NULL DEFAULT '0',
+  `findid` int(11) NOT NULL DEFAULT '0',
+  `deletepending` tinyint(1) NOT NULL DEFAULT '0',
+  `transcoder` int(11) NOT NULL DEFAULT '0',
+  `timestretch` float NOT NULL DEFAULT '1',
+  `recpriority` int(11) NOT NULL DEFAULT '0',
+  `basename` varchar(255) NOT NULL,
+  `progstart` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `progend` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
+  `playgroup` varchar(32) NOT NULL DEFAULT 'Default',
+  `profile` varchar(32) NOT NULL DEFAULT '',
+  `duplicate` tinyint(1) NOT NULL DEFAULT '0',
+  `transcoded` tinyint(1) NOT NULL DEFAULT '0',
+  `watched` tinyint(4) NOT NULL DEFAULT '0',
+  `storagegroup` varchar(32) NOT NULL DEFAULT 'Default',
+  `bookmarkupdate` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
+  PRIMARY KEY (`chanid`,`starttime`),
+  UNIQUE KEY `uni_recorded_basename` (`basename`),
+  KEY `endtime` (`endtime`),
+  KEY `seriesid` (`seriesid`),
+  KEY `programid` (`programid`),
+  KEY `title` (`title`),
+  KEY `recordid` (`recordid`),
+  KEY `deletepending` (`deletepending`,`lastmodified`),
+  KEY `recgroup` (`recgroup`,`endtime`)
+) ENGINE=MyISAM DEFAULT CHARSET=utf8;
+
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
+/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
+/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+-- Dump completed on 2015-12-24  5:08:30
+
diff --git a/mythfs/unittests/mockDefs.cpp b/mythfs/unittests/mockDefs.cpp
index 68dec08..ba233c3 100644
--- a/mythfs/unittests/mockDefs.cpp
+++ b/mythfs/unittests/mockDefs.cpp
@@ -2,16 +2,17 @@
 #include <definedDirs.h>
 
 Service::Service() :
-	MySQL::Mock("mythconverg", {
-			rootDir / "fixtures" / "schema.sql",
+	MySQL::Mock("MythFS", {
+			//rootDir / "fixtures" / "schema.sql",
+			rootDir / "fixtures" / "schema-min.sql",
 			rootDir / "fixtures" / "data.sql"
 		})
 {
 }
 
 TestClient::TestClient() :
-	db(getProxy<MythFS::DBPrx>("db")),
-	s(getProxy<NetFS::ServicePrx>("service"))
+	db(getProxy<MythFS::DBPrx>("DB")),
+	s(getProxy<NetFS::ServicePrx>("Service"))
 {
 }
 
diff --git a/mythfs/unittests/mockDefs.h b/mythfs/unittests/mockDefs.h
index 0b6248c..b1975e9 100644
--- a/mythfs/unittests/mockDefs.h
+++ b/mythfs/unittests/mockDefs.h
@@ -7,7 +7,7 @@
 #include <service.h>
 #include <myth-db.h>
 
-class DLL_PUBLIC Service : public IceTray::DryIce, MySQL::Mock {
+class DLL_PUBLIC Service : MySQL::Mock, public IceTray::DryIce {
 	public:
 		Service();
 };
diff --git a/mythfs/unittests/testMain.cpp b/mythfs/unittests/testMain.cpp
index 8c18647..5451630 100644
--- a/mythfs/unittests/testMain.cpp
+++ b/mythfs/unittests/testMain.cpp
@@ -4,6 +4,8 @@
 #include <Ice/Ice.h>
 #include <IceBox/IceBox.h>
 #include <service.h>
+#include <volume.h>
+#include <sys/stat.h>
 #include "mockDefs.h"
 
 BOOST_GLOBAL_FIXTURE(Service);
@@ -37,3 +39,61 @@ BOOST_AUTO_TEST_CASE( getRecorded )
 
 BOOST_AUTO_TEST_SUITE_END();
 
+class RecordingsTest : public TestClient {
+	public:
+		RecordingsTest() :
+			rv(s->connect("recordings", std::string()))
+		{
+			re.user = "root";
+			re.grp = "root";
+		}
+		~RecordingsTest()
+		{
+			rv->disconnect();
+		}
+
+	protected:
+		::NetFS::ReqEnv re;
+		::NetFS::VolumePrx rv;
+};
+
+BOOST_FIXTURE_TEST_SUITE(rt, RecordingsTest)
+
+BOOST_AUTO_TEST_CASE( statRoot )
+{
+	auto a = rv->getattr(re, "/");
+	BOOST_REQUIRE(S_ISDIR(a.mode));
+}
+
+BOOST_AUTO_TEST_CASE( listRoot )
+{
+	auto d = rv->opendir(re, "/");
+	auto ls = d->readdir();
+	BOOST_REQUIRE_EQUAL(1, ls.size());
+	BOOST_REQUIRE_EQUAL("all", ls.front());
+	d->close();
+}
+
+BOOST_AUTO_TEST_CASE( statAll )
+{
+	auto a = rv->getattr(re, "/all");
+	BOOST_REQUIRE(S_ISDIR(a.mode));
+}
+
+BOOST_AUTO_TEST_CASE( listAll )
+{
+	auto d = rv->opendir(re, "/all");
+	auto ls = d->readdir();
+	BOOST_REQUIRE_EQUAL(11, ls.size());
+	BOOST_REQUIRE_EQUAL("1001_20151220233900.mpg", ls.front());
+	BOOST_REQUIRE_EQUAL("1303_20151202205900.mpg", ls.back());
+	d->close();
+
+	auto a = rv->getattr(re, "/all/1001_20151220233900.mpg");
+	BOOST_REQUIRE(S_ISLNK(a.mode));
+	auto l = rv->readlink(re, "/all/1001_20151220233900.mpg");
+	BOOST_REQUIRE_EQUAL("/var/store/mythrecordings/1001_20151220233900.mpg", l);
+}
+
+BOOST_AUTO_TEST_SUITE_END();
+
diff --git a/mythfs/util.h b/mythfs/util.h
new file mode 100644
index 0000000..3d7ea35
--- /dev/null
+++ b/mythfs/util.h
@@ -0,0 +1,33 @@
+#ifndef MYTHFS_UTIL_H
+#define MYTHFS_UTIL_H
+
+template <typename T, typename MT>
+std::vector<MT>
+operator/(const T & input, MT T::value_type::* m)
+{
+	std::vector<MT> result;
+	/*
+	result.reserve(input.size());
+	for (const auto & i : input) {
+		result.push_back(i.*m);
+	}
+	*/
+	return result;
+}
+
+/*
+template <typename T, typename MT>
+std::vector<std::string>
+operator/(const T & input, MT T::value_type::element_type::* m)
+{
+	std::vector<MT> result;
+	result.reserve(input.size());
+	for (const auto & i : input) {
+		result.push_back(i.get()->*m);
+	}
+	return result;
+}
+*/
+
+#endif
+
-- 
cgit v1.2.3