summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
Diffstat (limited to 'apps')
-rw-r--r--apps/files_versioning/ajax/gethead.php12
-rw-r--r--apps/files_versioning/ajax/sethead.php14
-rw-r--r--apps/files_versioning/appinfo/app.php20
-rw-r--r--apps/files_versioning/appinfo/info.xml14
-rw-r--r--apps/files_versioning/css/settings.css3
-rw-r--r--apps/files_versioning/js/settings.js25
-rw-r--r--apps/files_versioning/lib_granite.php12
-rw-r--r--apps/files_versioning/settings.php34
-rw-r--r--apps/files_versioning/templates/settings.php12
-rw-r--r--apps/files_versioning/versionstorage.php386
-rw-r--r--apps/files_versioning/versionwrapper.php686
11 files changed, 1218 insertions, 0 deletions
diff --git a/apps/files_versioning/ajax/gethead.php b/apps/files_versioning/ajax/gethead.php
new file mode 100644
index 00000000000..cc93b7a1d17
--- /dev/null
+++ b/apps/files_versioning/ajax/gethead.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * Copyright (c) 2011 Craig Roberts craig0990@googlemail.com
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ */
+require_once('../../../lib/base.php');
+
+OC_JSON::checkLoggedIn();
+// Fetch current commit (or HEAD if not yet set)
+$head = OC_Preferences::getValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+OC_JSON::encodedPrint(array("head" => $head));
diff --git a/apps/files_versioning/ajax/sethead.php b/apps/files_versioning/ajax/sethead.php
new file mode 100644
index 00000000000..d1b2df9b00f
--- /dev/null
+++ b/apps/files_versioning/ajax/sethead.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * Copyright (c) 2011 Craig Roberts craig0990@googlemail.com
+ * This file is licensed under the Affero General Public License version 3 or
+ * later.
+ */
+require_once('../../../lib/base.php');
+OC_JSON::checkLoggedIn();
+if(isset($_POST["file_versioning_head"])){
+ OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', $_POST["file_versioning_head"]);
+ OC_JSON::success();
+}else{
+ OC_JSON::error();
+}
diff --git a/apps/files_versioning/appinfo/app.php b/apps/files_versioning/appinfo/app.php
new file mode 100644
index 00000000000..24a8701dbb0
--- /dev/null
+++ b/apps/files_versioning/appinfo/app.php
@@ -0,0 +1,20 @@
+<?php
+
+// Include required files
+require_once('apps/files_versioning/versionstorage.php');
+require_once('apps/files_versioning/versionwrapper.php');
+// Register streamwrapper for versioned:// paths
+stream_wrapper_register('versioned', 'OC_VersionStreamWrapper');
+
+// Add an entry in the app list for versioning and backup
+OC_App::register( array(
+ 'order' => 10,
+ 'id' => 'files_versioning',
+ 'name' => 'Versioning and Backup' ));
+
+// Include stylesheets for the settings page
+OC_Util::addStyle( 'files_versioning', 'settings' );
+OC_Util::addScript('files_versioning','settings');
+
+// Register a settings section in the Admin > Personal page
+OC_APP::registerPersonal('files_versioning','settings');
diff --git a/apps/files_versioning/appinfo/info.xml b/apps/files_versioning/appinfo/info.xml
new file mode 100644
index 00000000000..d5546be54ae
--- /dev/null
+++ b/apps/files_versioning/appinfo/info.xml
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<info>
+ <id>files_versioning</id>
+ <name>Versioning and Backup</name>
+ <version>1.0.0</version>
+ <licence>GPLv2</licence>
+ <author>Craig Roberts</author>
+ <require>3</require>
+ <description>Versions files using Git repositories, providing a simple backup facility. Currently in *beta* and explicitly without warranty of any kind.</description>
+ <default_enable/>
+ <types>
+ <filesystem/>
+ </types>
+</info>
diff --git a/apps/files_versioning/css/settings.css b/apps/files_versioning/css/settings.css
new file mode 100644
index 00000000000..afe2cd5508f
--- /dev/null
+++ b/apps/files_versioning/css/settings.css
@@ -0,0 +1,3 @@
+#file_versioning_commit_chzn {
+ width: 15em;
+}
diff --git a/apps/files_versioning/js/settings.js b/apps/files_versioning/js/settings.js
new file mode 100644
index 00000000000..8dd13bac033
--- /dev/null
+++ b/apps/files_versioning/js/settings.js
@@ -0,0 +1,25 @@
+$(document).ready(function(){
+ $('#file_versioning_head').chosen();
+
+ $.getJSON(OC.filePath('files_versioning', 'ajax', 'gethead.php'), function(jsondata, status) {
+
+ if (jsondata.head == 'HEAD') {
+ // Most recent commit, do nothing
+ } else {
+ $("#file_versioning_head").val(jsondata.head);
+ // Trigger the chosen update call
+ // See http://harvesthq.github.com/chosen/
+ $("#file_versioning_head").trigger("liszt:updated");
+ }
+ });
+
+ $('#file_versioning_head').change(function() {
+
+ var data = $(this).serialize();
+ $.post( OC.filePath('files_versioning', 'ajax', 'sethead.php'), data, function(data){
+ if(data == 'error'){
+ console.log('Saving new HEAD failed');
+ }
+ });
+ });
+});
diff --git a/apps/files_versioning/lib_granite.php b/apps/files_versioning/lib_granite.php
new file mode 100644
index 00000000000..c69c62d9c45
--- /dev/null
+++ b/apps/files_versioning/lib_granite.php
@@ -0,0 +1,12 @@
+<?php
+
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/blob.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/commit.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/repository.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/tag.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/tree.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/tree/node.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/index.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/raw.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/loose.php');
+require_once(OC::$SERVERROOT . '/3rdparty/granite/git/object/packed.php');
diff --git a/apps/files_versioning/settings.php b/apps/files_versioning/settings.php
new file mode 100644
index 00000000000..94af587a215
--- /dev/null
+++ b/apps/files_versioning/settings.php
@@ -0,0 +1,34 @@
+<?php
+
+// Get the full path to the repository folder (FIXME: hard-coded to 'Backup')
+$path = OC_Config::getValue('datadirectory', OC::$SERVERROOT.'/data')
+ . DIRECTORY_SEPARATOR
+ . OC_User::getUser()
+ . DIRECTORY_SEPARATOR
+ . 'files'
+ . DIRECTORY_SEPARATOR
+ . 'Backup'
+ . DIRECTORY_SEPARATOR
+ . '.git'
+ . DIRECTORY_SEPARATOR;
+
+$repository = new Granite\Git\Repository($path);
+
+$commits = array();
+// Fetch most recent 50 commits (FIXME - haven't tested this much)
+$commit = $repository->head();
+for ($i = 0; $i < 50; $i++) {
+ $commits[] = $commit;
+ $parents = $commit->parents();
+ if (count($parents) > 0) {
+ $parent = $parents[0];
+ } else {
+ break;
+ }
+
+ $commit = $repository->factory('commit', $parent);
+}
+
+$tmpl = new OC_Template( 'files_versioning', 'settings');
+$tmpl->assign('commits', $commits);
+return $tmpl->fetchPage();
diff --git a/apps/files_versioning/templates/settings.php b/apps/files_versioning/templates/settings.php
new file mode 100644
index 00000000000..17f4cc7f77f
--- /dev/null
+++ b/apps/files_versioning/templates/settings.php
@@ -0,0 +1,12 @@
+<fieldset id="status_list" class="personalblock">
+ <strong>Versioning and Backup</strong><br>
+ <p><em>Please note: Backing up large files (around 16MB+) will cause your backup history to grow very large, very quickly.</em></p>
+ <label class="bold">Backup Folder</label>
+ <select name="file_versioning_head" id="file_versioning_head">
+ <?php
+ foreach ($_['commits'] as $commit):
+ echo '<option value="' . $commit->sha() . '">' . $commit->message() . '</option>';
+ endforeach;
+ ?>
+ </select>
+</fieldset>
diff --git a/apps/files_versioning/versionstorage.php b/apps/files_versioning/versionstorage.php
new file mode 100644
index 00000000000..d083e623df9
--- /dev/null
+++ b/apps/files_versioning/versionstorage.php
@@ -0,0 +1,386 @@
+<?php
+/**
+ * ownCloud file storage implementation for Git repositories
+ * @author Craig Roberts
+ * @copyright 2012 Craig Roberts craig0990@googlemail.com
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or 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 AFFERO GENERAL PUBLIC LICENSE for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public
+ * License along with this library. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+// Include Granite
+require_once('lib_granite.php');
+
+// Create a top-level 'Backup' directory if it does not already exist
+$user = OC_User::getUser();
+if (OC_Filesystem::$loaded and !OC_Filesystem::is_dir('/Backup')) {
+ OC_Filesystem::mkdir('/Backup');
+ OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+}
+
+// Generate the repository path (currently using 'full' repositories, as opposed to bare ones)
+$repo_path = DIRECTORY_SEPARATOR
+ . OC_User::getUser()
+ . DIRECTORY_SEPARATOR
+ . 'files'
+ . DIRECTORY_SEPARATOR
+ . 'Backup';
+
+// Mount the 'Backup' folder using the versioned storage provider below
+OC_Filesystem::mount('OC_Filestorage_Versioned', array('repo'=>$repo_path), $repo_path . DIRECTORY_SEPARATOR);
+
+class OC_Filestorage_Versioned extends OC_Filestorage {
+
+ /**
+ * Holds an instance of Granite\Git\Repository
+ */
+ protected $repo;
+
+ /**
+ * Constructs a new OC_Filestorage_Versioned instance, expects an associative
+ * array with a `repo` key set to the path of the repository's `.git` folder
+ *
+ * @param array $parameters An array containing the key `repo` pointing to the
+ * repository path.
+ */
+ public function __construct($parameters) {
+ // Get the full path to the repository folder
+ $path = OC_Config::getValue('datadirectory', OC::$SERVERROOT.'/data')
+ . $parameters['repo']
+ . DIRECTORY_SEPARATOR
+ . '.git'
+ . DIRECTORY_SEPARATOR;
+
+ try {
+ // Attempt to load the repository
+ $this->repo = new Granite\Git\Repository($path);
+ } catch (InvalidArgumentException $e) {
+ // $path is not a valid Git repository, we must create one
+ Granite\Git\Repository::init($path);
+
+ // Load the newly-initialised repository
+ $this->repo = new Granite\Git\Repository($path);
+
+ /**
+ * Create an initial commit with a README file
+ * FIXME: This functionality should be transferred to the Granite library
+ */
+ $blob = new Granite\Git\Blob($this->repo->path());
+ $blob->content('Your Backup directory is now ready for use.');
+
+ // Create a new tree to hold the README file
+ $tree = $this->repo->factory('tree');
+ // Create a tree node to represent the README blob
+ $tree_node = new Granite\Git\Tree\Node('README', '100644', $blob->sha());
+ $tree->nodes(array($tree_node->name() => $tree_node));
+
+ // Create an initial commit
+ $commit = new Granite\Git\Commit($this->repo->path());
+ $user_string = OC_User::getUser() . ' ' . time() . ' +0000';
+ $commit->author($user_string);
+ $commit->committer($user_string);
+ $commit->message('Initial commit');
+ $commit->tree($tree);
+
+ // Write it all to disk
+ $blob->write();
+ $tree->write();
+ $commit->write();
+
+ // Update the HEAD for the 'master' branch
+ $this->repo->head('master', $commit->sha());
+ }
+
+ // Update the class pointer to the HEAD
+ $head = OC_Preferences::getValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+
+ // Load the most recent commit if the preference is not set
+ if ($head == 'HEAD') {
+ $this->head = $this->repo->head()->sha();
+ } else {
+ $this->head = $head;
+ }
+ }
+
+ public function mkdir($path) {
+ if (mkdir("versioned:/{$this->repo->path()}$path#{$this->head}")) {
+ $this->head = $this->repo->head()->sha();
+ OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', $head);
+ return true;
+ }
+
+ return false;
+ }
+
+ public function rmdir($path) {
+
+ }
+
+ /**
+ * Returns a directory handle to the requested path, or FALSE on failure
+ *
+ * @param string $path The directory path to open
+ *
+ * @return boolean|resource A directory handle, or FALSE on failure
+ */
+ public function opendir($path) {
+ return opendir("versioned:/{$this->repo->path()}$path#{$this->head}");
+ }
+
+ /**
+ * Returns TRUE if $path is a directory, or FALSE if not
+ *
+ * @param string $path The path to check
+ *
+ * @return boolean
+ */
+ public function is_dir($path) {
+ return $this->filetype($path) == 'dir';
+ }
+
+ /**
+ * Returns TRUE if $path is a file, or FALSE if not
+ *
+ * @param string $path The path to check
+ *
+ * @return boolean
+ */
+ public function is_file($path) {
+ return $this->filetype($path) == 'file';
+ }
+
+ public function stat($path)
+ {
+ return stat("versioned:/{$this->repo->path()}$path#{$this->head}");
+ }
+
+ /**
+ * Returns the strings 'dir' or 'file', depending on the type of $path
+ *
+ * @param string $path The path to check
+ *
+ * @return string Returns 'dir' if a directory, 'file' otherwise
+ */
+ public function filetype($path) {
+ if ($path == "" || $path == "/") {
+ return 'dir';
+ } else {
+ if (substr($path, -1) == '/') {
+ $path = substr($path, 0, -1);
+ }
+
+ $node = $this->tree_search($this->repo, $this->repo->factory('commit', $this->head)->tree(), $path);
+
+ // Does it exist, or is it new?
+ if ($node == null) {
+ // New file
+ return 'file';
+ } else {
+ // Is it a tree?
+ try {
+ $this->repo->factory('tree', $node);
+ return 'dir';
+ } catch (InvalidArgumentException $e) {
+ // Nope, must be a blob
+ return 'file';
+ }
+ }
+ }
+ }
+
+ public function filesize($path) {
+ return filesize("versioned:/{$this->repo->path()}$path#{$this->head}");
+ }
+
+ /**
+ * Returns a boolean value representing whether $path is readable
+ *
+ * @param string $path The path to check
+ *(
+ * @return boolean Whether or not the path is readable
+ */
+ public function is_readable($path) {
+ return true;
+ }
+
+ /**
+ * Returns a boolean value representing whether $path is writable
+ *
+ * @param string $path The path to check
+ *(
+ * @return boolean Whether or not the path is writable
+ */
+ public function is_writable($path) {
+
+ $head = OC_Preferences::getValue(OC_User::getUser(), 'files_versioning', 'head', 'HEAD');
+ if ($head !== 'HEAD' && $head !== $this->repo->head()->sha()) {
+ // Cannot modify previous commits
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Returns a boolean value representing whether $path exists
+ *
+ * @param string $path The path to check
+ *(
+ * @return boolean Whether or not the path exists
+ */
+ public function file_exists($path) {
+ return file_exists("versioned:/{$this->repo->path()}$path#{$this->head}");
+ }
+
+ /**
+ * Returns an integer value representing the inode change time
+ * (NOT IMPLEMENTED)
+ *
+ * @param string $path The path to check
+ *(
+ * @return int Timestamp of the last inode change
+ */
+ public function filectime($path) {
+ return -1;
+ }
+
+ /**
+ * Returns an integer value representing the file modification time
+ *
+ * @param string $path The path to check
+ *(
+ * @return int Timestamp of the last file modification
+ */
+ public function filemtime($path) {
+ return filemtime("versioned:/{$this->repo->path()}$path#{$this->head}");
+ }
+
+ public function file_get_contents($path) {
+ return file_get_contents("versioned:/{$this->repo->path()}$path#{$this->head}");
+ }
+
+ public function file_put_contents($path, $data) {
+ $success = file_put_contents("versioned:/{$this->repo->path()}$path#{$this->head}", $data);
+ if ($success !== false) {
+ // Update the HEAD in the preferences
+ OC_Preferences::setValue(OC_User::getUser(), 'files_versioning', 'head', $this->repo->head()->sha());
+ return $success;
+ }
+
+ return false;
+ }
+
+ public function unlink($path) {
+
+ }
+
+ public function rename($path1, $path2) {
+
+ }
+
+ public function copy($path1, $path2) {
+
+ }
+
+ public function fopen($path, $mode) {
+ return fopen("versioned:/{$this->repo->path()}$path#{$this->head}", $mode);
+ }
+
+ public function getMimeType($path) {
+ if ($this->filetype($path) == 'dir') {
+ return 'httpd/unix-directory';
+ } elseif ($this->filesize($path) == 0) {
+ // File's empty, returning text/plain allows opening in the web editor
+ return 'text/plain';
+ } else {
+ $finfo = new finfo(FILEINFO_MIME_TYPE);
+ /**
+ * We need to represent the repository path, the file path, and the
+ * revision, which can be simply achieved with a convention of using
+ * `.git` in the repository directory (bare or not) and the '#part'
+ * segment of a URL to specify the revision. For example
+ *
+ * versioned://var/www/myrepo.git/docs/README.md#HEAD ('bare' repo)
+ * versioned://var/www/myrepo/.git/docs/README.md#HEAD ('full' repo)
+ * versioned://var/www/myrepo/.git/docs/README.md#6a8f...8a54 ('full' repo and SHA-1 commit ID)
+ */
+ $mime = $finfo->buffer(file_get_contents("versioned:/{$this->repo->path()}$path#{$this->head}"));
+ return $mime;
+ }
+ }
+
+ /**
+ * Generates a hash based on the file contents
+ *
+ * @param string $type The hashing algorithm to use (e.g. 'md5', 'sha256', etc.)
+ * @param string $path The file to be hashed
+ * @param boolean $raw Outputs binary data if true, lowercase hex digits otherwise
+ *
+ * @return string Hashed string representing the file contents
+ */
+ public function hash($type, $path, $raw) {
+ return hash($type, file_get_contents($path), $raw);
+ }
+
+ public function free_space($path) {
+ }
+
+ public function search($query) {
+
+ }
+
+ public function touch($path, $mtime=null) {
+
+ }
+
+
+ public function getLocalFile($path) {
+ }
+
+ /**
+ * Recursively searches a tree for a path, returning FALSE if is not found
+ * or an SHA-1 id if it is found.
+ *
+ * @param string $repo The repository containing the tree object
+ * @param string $tree The tree object to search
+ * @param string $path The path to search for (relative to the tree)
+ * @param int $depth The depth of the current search (for recursion)
+ *
+ * @return string|boolean The SHA-1 id of the sub-tree
+ */
+ private function tree_search($repo, $tree, $path, $depth = 0)
+ {
+ $paths = array_values(explode(DIRECTORY_SEPARATOR, $path));
+
+ $current_path = $paths[$depth];
+
+ $nodes = $tree->nodes();
+ foreach ($nodes as $node) {
+ if ($node->name() == $current_path) {
+
+ if (count($paths)-1 == $depth) {
+ // Stop, found it
+ return $node->sha();
+ }
+
+ // Recurse if necessary
+ if ($node->isDirectory()) {
+ $tree = $this->repo->factory('tree', $node->sha());
+ return $this->tree_search($repo, $tree, $path, $depth + 1);
+ }
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/apps/files_versioning/versionwrapper.php b/apps/files_versioning/versionwrapper.php
new file mode 100644
index 00000000000..b83a4fd3b22
--- /dev/null
+++ b/apps/files_versioning/versionwrapper.php
@@ -0,0 +1,686 @@
+<?php
+
+final class OC_VersionStreamWrapper {
+
+ /**
+ * Determines whether or not to log debug messages with `OC_Log::write()`
+ */
+ private $debug = true;
+
+ /**
+ * The name of the ".empty" files created in new directories
+ */
+ const EMPTYFILE = '.empty';
+
+ /**
+ * Stores the current position for `readdir()` etc. calls
+ */
+ private $dir_position = 0;
+
+ /**
+ * Stores the current position for `fread()`, `fseek()` etc. calls
+ */
+ private $file_position = 0;
+
+ /**
+ * Stores the current directory tree for `readdir()` etc. directory traversal
+ */
+ private $tree;
+
+ /**
+ * Stores the current file for `fread()`, `fseek()`, etc. calls
+ */
+ private $blob;
+
+ /**
+ * Stores the current commit for `fstat()`, `stat()`, etc. calls
+ */
+ private $commit;
+
+ /**
+ * Stores the current path for `fwrite()`, `file_put_contents()` etc. calls
+ */
+ private $path;
+
+ /**
+ * Close directory handle
+ */
+ public function dir_closedir() {
+ unset($this->tree);
+ return true;
+ }
+
+ /**
+ * Open directory handle
+ */
+ public function dir_opendir($path, $options) {
+ // Parse the URL into a repository directory, file path and commit ID
+ list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+ if ($repo_file == '' || $repo_file == '/') {
+ // Set the tree property for the future `readdir()` etc. calls
+ $this->tree = array_values($this->commit->tree()->nodes());
+ return true;
+ } elseif ($this->tree_search($this->repo, $this->commit->tree(), $repo_file) !== false) {
+ // Something exists at this path, is it a directory though?
+ try {
+ $tree = $this->repo->factory(
+ 'tree',
+ $this->tree_search($this->repo, $this->commit->tree(), $repo_file)
+ );
+ $this->tree = array_values($tree->nodes());
+ return true;
+ } catch (InvalidArgumentException $e) {
+ // Trying to call `opendir()` on a file, return false below
+ }
+ }
+
+ // Unable to find the directory, return false
+ return false;
+ }
+
+ /**
+ * Read entry from directory handle
+ */
+ public function dir_readdir() {
+ return isset($this->tree[$this->dir_position])
+ ? $this->tree[$this->dir_position++]->name()
+ : false;
+ }
+
+ /**
+ * Rewind directory handle
+ */
+ public function dir_rewinddir() {
+ $this->dir_position = 0;
+ }
+
+ /**
+ * Create a directory
+ * Git doesn't track empty directories, so a ".empty" file is added instead
+ */
+ public function mkdir($path, $mode, $options) {
+ // Parse the URL into a repository directory, file path and commit ID
+ list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+ // Create an empty file for Git
+ $empty = new Granite\Git\Blob($this->repo->path());
+ $empty->content('');
+ $empty->write();
+
+ if (dirname($repo_file) == '.') {
+ // Adding a new directory to the root tree
+ $tree = $this->repo->head()->tree();
+ } else {
+ $tree = $this->repo->factory('tree', $this->tree_search(
+ $this->repo, $this->repo->head()->tree(), dirname($repo_file)
+ )
+ );
+ }
+
+ // Create our new tree, with our empty file
+ $dir = $this->repo->factory('tree');
+ $nodes = array();
+ $nodes[self::EMPTYFILE] = new Granite\Git\Tree\Node(self::EMPTYFILE, '100644', $empty->sha());
+ $dir->nodes($nodes);
+ $dir->write();
+
+ // Add our new tree to its parent
+ $nodes = $tree->nodes();
+ $nodes[basename($repo_file)] = new Granite\Git\Tree\Node(basename($repo_file), '040000', $dir->sha());
+ $tree->nodes($nodes);
+ $tree->write();
+
+ // We need to recursively update each parent tree, since they are all
+ // hashed and the changes will cascade back up the chain
+
+ // So, we're currently at the bottom-most directory
+ $current_dir = dirname($repo_file);
+ $previous_tree = $tree;
+
+ if ($current_dir !== '.') {
+ do {
+ // Determine the parent directory
+ $previous_dir = $current_dir;
+ $current_dir = dirname($current_dir);
+
+ $current_tree = $current_dir !== '.'
+ ? $this->repo->factory(
+ 'tree', $this->tree_search(
+ $this->repo,
+ $this->repo->head()->tree(),
+ $current_dir
+ )
+ )
+ : $this->repo->head()->tree();
+
+ $current_nodes = $current_tree->nodes();
+ $current_nodes[basename($previous_dir)] = new Granite\Git\Tree\Node(
+ basename($previous_dir), '040000', $previous_tree->sha()
+ );
+ $current_tree->nodes($current_nodes);
+ $current_tree->write();
+
+ $previous_tree = $current_tree;
+ } while ($current_dir !== '.');
+
+ $tree = $previous_tree;
+ }
+
+ // Create a new commit to represent this write
+ $commit = $this->repo->factory('commit');
+ $username = OC_User::getUser();
+ $user_string = $username . ' ' . time() . ' +0000';
+ $commit->author($user_string);
+ $commit->committer($user_string);
+ $commit->message("$username created the `$repo_file` directory, " . date('d F Y H:i', time()) . '.');
+ $commit->parents(array($this->repo->head()->sha()));
+ $commit->tree($tree);
+
+ // Write it to disk
+ $commit->write();
+
+ // Update the HEAD for the 'master' branch
+ $this->repo->head('master', $commit->sha());
+
+ return true;
+ }
+
+ /**
+ * Renames a file or directory
+ */
+ public function rename($path_from, $path_to) {
+
+ }
+
+ /**
+ * Removes a directory
+ */
+ public function rmdir($path, $options) {
+
+ }
+
+ /**
+ * Retrieve the underlaying resource (NOT IMPLEMENTED)
+ */
+ public function stream_cast($cast_as) {
+ return false;
+ }
+
+ /**
+ * Close a resource
+ */
+ public function stream_close() {
+ unset($this->blob);
+ return true;
+ }
+
+ /**
+ * Tests for end-of-file on a file pointer
+ */
+ public function stream_eof() {
+ return !($this->file_position < strlen($this->blob));
+ }
+
+ /**
+ * Flushes the output (NOT IMPLEMENTED)
+ */
+ public function stream_flush() {
+ return false;
+ }
+
+ /**
+ * Advisory file locking (NOT IMPLEMENTED)
+ */
+ public function stream_lock($operation) {
+ return false;
+ }
+
+ /**
+ * Change stream options (NOT IMPLEMENTED)
+ * Called in response to `chgrp()`, `chown()`, `chmod()` and `touch()`
+ */
+ public function stream_metadata($path, $option, $var) {
+ return false;
+ }
+
+ /**
+ * Opens file or URL
+ */
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ // Store the path, so we can use it later in `stream_write()` if necessary
+ $this->path = $path;
+ // Parse the URL into a repository directory, file path and commit ID
+ list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+ $file = $this->tree_search($this->repo, $this->commit->tree(), $repo_file);
+ if ($file !== false) {
+ try {
+ $this->blob = $this->repo->factory('blob', $file)->content();
+ return true;
+ } catch (InvalidArgumentException $e) {
+ // Trying to open a directory, return false below
+ }
+ } elseif ($mode !== 'r') {
+ // All other modes allow opening for reading and writing, clearly
+ // some 'write' files may not exist yet...
+ return true;
+ }
+
+ // File could not be found or is not actually a file
+ return false;
+ }
+
+ /**
+ * Read from stream
+ */
+ public function stream_read($count) {
+ // Fetch the remaining set of bytes
+ $bytes = substr($this->blob, $this->file_position, $count);
+
+ // If EOF or empty string, return false
+ if ($bytes == '' || $bytes == false) {
+ return false;
+ }
+
+ // If $count does not extend past EOF, add $count to stream offset
+ if ($this->file_position + $count < strlen($this->blob)) {
+ $this->file_position += $count;
+ } else {
+ // Otherwise return all remaining bytes
+ $this->file_position = strlen($this->blob);
+ }
+
+ return $bytes;
+ }
+
+ /**
+ * Seeks to specific location in a stream
+ */
+ public function stream_seek($offset, $whence = SEEK_SET) {
+ $new_offset = false;
+
+ switch ($whence)
+ {
+ case SEEK_SET:
+ $new_offset = $offset;
+ break;
+ case SEEK_CUR:
+ $new_offset = $this->file_position += $offset;
+ break;
+ case SEEK_END:
+ $new_offset = strlen($this->blob) + $offset;
+ break;
+ }
+
+ $this->file_position = $offset;
+
+ return ($new_offset !== false);
+ }
+
+ /**
+ * Change stream options (NOT IMPLEMENTED)
+ */
+ public function stream_set_option($option, $arg1, $arg2) {
+ return false;
+ }
+
+ /**
+ * Retrieve information about a file resource (NOT IMPLEMENTED)
+ */
+ public function stream_stat() {
+
+ }
+
+ /**
+ * Retrieve the current position of a stream
+ */
+ public function stream_tell() {
+ return $this->file_position;
+ }
+
+ /**
+ * Truncate stream
+ */
+ public function stream_truncate($new_size) {
+
+ }
+
+ /**
+ * Write to stream
+ * FIXME: Could use heavy refactoring
+ */
+ public function stream_write($data) {
+ /**
+ * FIXME: This also needs to be added to Granite, in the form of `add()`,
+ * `rm()` and `commit()` calls
+ */
+
+ // Parse the URL into a repository directory, file path and commit ID
+ list($this->repo, $repo_file, $this->commit) = $this->parse_url($this->path);
+
+ $node = $this->tree_search($this->repo, $this->commit->tree(), $repo_file);
+
+ if ($node !== false) {
+ // File already exists, attempting modification of existing tree
+ try {
+ $this->repo->factory('blob', $node);
+
+ // Create our new blob with the provided $data
+ $blob = $this->repo->factory('blob');
+ $blob->content($data);
+ $blob->write();
+
+ // We know the tree exists, so strip the filename from the path and
+ // find it...
+
+ if (dirname($repo_file) == '.' || dirname($repo_file) == '') {
+ // Root directory
+ $tree = $this->repo->head()->tree();
+ } else {
+ // Sub-directory
+ $tree = $this->repo->factory('tree', $this->tree_search(
+ $this->repo,
+ $this->repo->head()->tree(),
+ dirname($repo_file)
+ )
+ );
+ }
+
+ // Replace the old blob with our newly modified one
+ $tree_nodes = $tree->nodes();
+ $tree_nodes[basename($repo_file)] = new Granite\Git\Tree\Node(
+ basename($repo_file), '100644', $blob->sha()
+ );
+ $tree->nodes($tree_nodes);
+ $tree->write();
+
+ // We need to recursively update each parent tree, since they are all
+ // hashed and the changes will cascade back up the chain
+
+ // So, we're currently at the bottom-most directory
+ $current_dir = dirname($repo_file);
+ $previous_tree = $tree;
+
+ if ($current_dir !== '.') {
+ do {
+ // Determine the parent directory
+ $previous_dir = $current_dir;
+ $current_dir = dirname($current_dir);
+
+ $current_tree = $current_dir !== '.'
+ ? $this->repo->factory(
+ 'tree', $this->tree_search(
+ $this->repo,
+ $this->repo->head()->tree(),
+ $current_dir
+ )
+ )
+ : $this->repo->head()->tree();
+
+ $current_nodes = $current_tree->nodes();
+ $current_nodes[basename($previous_dir)] = new Granite\Git\Tree\Node(
+ basename($previous_dir), '040000', $previous_tree->sha()
+ );
+ $current_tree->nodes($current_nodes);
+ $current_tree->write();
+
+ $previous_tree = $current_tree;
+ } while ($current_dir !== '.');
+ }
+
+ // Create a new commit to represent this write
+ $commit = $this->repo->factory('commit');
+ $username = OC_User::getUser();
+ $user_string = $username . ' ' . time() . ' +0000';
+ $commit->author($user_string);
+ $commit->committer($user_string);
+ $commit->message("$username modified the `$repo_file` file, " . date('d F Y H:i', time()) . '.');
+ $commit->parents(array($this->repo->head()->sha()));
+ $commit->tree($previous_tree);
+
+ // Write it to disk
+ $commit->write();
+
+ // Update the HEAD for the 'master' branch
+ $this->repo->head('master', $commit->sha());
+
+ // If we made it this far, write was successful - update the stream
+ // position and return the number of bytes written
+ $this->file_position += strlen($data);
+ return strlen($data);
+
+ } catch (InvalidArgumentException $e) {
+ // Attempting to write to a directory or other error, fail
+ return 0;
+ }
+ } else {
+ // File does not exist, needs to be created
+
+ // Create our new blob with the provided $data
+ $blob = $this->repo->factory('blob');
+ $blob->content($data);
+ $blob->write();
+
+ if (dirname($repo_file) == '.') {
+ // Trying to add a new file to the root tree, nice and easy
+ $tree = $this->repo->head()->tree();
+ $tree_nodes = $tree->nodes();
+ $tree_nodes[basename($repo_file)] = new Granite\Git\Tree\Node(
+ basename($repo_file), '100644', $blob->sha()
+ );
+ $tree->nodes($tree_nodes);
+ $tree->write();
+ } else {
+ // Trying to add a new file to a subdirectory, try and find it
+ $tree = $this->repo->factory('tree', $this->tree_search(
+ $this->repo, $this->repo->head()->tree(), dirname($repo_file)
+ )
+ );
+
+ // Add the blob to the tree
+ $nodes = $tree->nodes();
+ $nodes[basename($repo_file)] = new Granite\Git\Tree\Node(
+ basename($repo_file), '100644', $blob->sha()
+ );
+ $tree->nodes($nodes);
+ $tree->write();
+
+ // We need to recursively update each parent tree, since they are all
+ // hashed and the changes will cascade back up the chain
+
+ // So, we're currently at the bottom-most directory
+ $current_dir = dirname($repo_file);
+ $previous_tree = $tree;
+
+ if ($current_dir !== '.') {
+ do {
+ // Determine the parent directory
+ $previous_dir = $current_dir;
+ $current_dir = dirname($current_dir);
+
+ $current_tree = $current_dir !== '.'
+ ? $this->repo->factory(
+ 'tree', $this->tree_search(
+ $this->repo,
+ $this->repo->head()->tree(),
+ $current_dir
+ )
+ )
+ : $this->repo->head()->tree();
+
+ $current_nodes = $current_tree->nodes();
+ $current_nodes[basename($previous_dir)] = new Granite\Git\Tree\Node(
+ basename($previous_dir), '040000', $previous_tree->sha()
+ );
+ $current_tree->nodes($current_nodes);
+ $current_tree->write();
+
+ $previous_tree = $current_tree;
+ } while ($current_dir !== '.');
+
+ $tree = $previous_tree;
+ }
+ }
+
+ // Create a new commit to represent this write
+ $commit = $this->repo->factory('commit');
+ $username = OC_User::getUser();
+ $user_string = $username . ' ' . time() . ' +0000';
+ $commit->author($user_string);
+ $commit->committer($user_string);
+ $commit->message("$username created the `$repo_file` file, " . date('d F Y H:i', time()) . '.');
+ $commit->parents(array($this->repo->head()->sha()));
+ $commit->tree($tree); // Top-level tree (NOT the newly modified tree)
+
+ // Write it to disk
+ $commit->write();
+
+ // Update the HEAD for the 'master' branch
+ $this->repo->head('master', $commit->sha());
+
+ // If we made it this far, write was successful - update the stream
+ // position and return the number of bytes written
+ $this->file_position += strlen($data);
+ return strlen($data);
+ }
+
+ // Write failed
+ return 0;
+ }
+
+ /**
+ * Delete a file
+ */
+ public function unlink($path) {
+
+ }
+
+ /**
+ * Retrieve information about a file
+ */
+ public function url_stat($path, $flags) {
+ // Parse the URL into a repository directory, file path and commit ID
+ list($this->repo, $repo_file, $this->commit) = $this->parse_url($path);
+
+ $node = $this->tree_search($this->repo, $this->commit->tree(), $repo_file);
+
+ if ($node == false && $this->commit->sha() == $this->repo->head()->sha()) {
+ // A new file - no information available
+ $size = 0;
+ $mtime = -1;
+ } else {
+
+ // Is it a directory?
+ try {
+ $this->repo->factory('tree', $node);
+ $size = 4096; // FIXME
+ } catch (InvalidArgumentException $e) {
+ // Must be a file
+ $size = strlen(file_get_contents($path));
+ }
+
+ // Parse the timestamp from the commit message
+ preg_match('/[0-9]{10}+/', $this->commit->committer(), $matches);
+ $mtime = $matches[0];
+ }
+
+ $stat["dev"] = "";
+ $stat["ino"] = "";
+ $stat["mode"] = "";
+ $stat["nlink"] = "";
+ $stat["uid"] = "";
+ $stat["gid"] = "";
+ $stat["rdev"] = "";
+ $stat["size"] = $size;
+ $stat["atime"] = $mtime;
+ $stat["mtime"] = $mtime;
+ $stat["ctime"] = $mtime;
+ $stat["blksize"] = "";
+ $stat["blocks"] = "";
+
+ return $stat;
+ }
+
+ /**
+ * Debug function for development purposes
+ */
+ private function debug($message, $level = OC_Log::DEBUG)
+ {
+ if ($this->debug) {
+ OC_Log::write('files_versioning', $message, $level);
+ }
+ }
+
+ /**
+ * Parses a URL of the form:
+ * `versioned://path/to/git/repository/.git/path/to/file#SHA-1-commit-id`
+ * FIXME: Will throw an InvalidArgumentException if $path is invaid
+ *
+ * @param string $path The path to parse
+ *
+ * @return array An array containing an instance of Granite\Git\Repository,
+ * the file path, and an instance of Granite\Git\Commit
+ * @throws InvalidArgumentException If the repository cannot be loaded
+ */
+ private function parse_url($path)
+ {
+ preg_match('/\/([A-Za-z0-9\/]+\.git\/)([A-Za-z0-9\/\.\/]*)(#([A-Fa-f0-9]+))*/', $path, $matches);
+
+ // Load up the repo
+ $repo = new \Granite\Git\Repository($matches[1]);
+ // Parse the filename (stripping any trailing slashes)
+ $repo_file = $matches[2];
+ if (substr($repo_file, -1) == '/') {
+ $repo_file = substr($repo_file, 0, -1);
+ }
+
+ // Default to HEAD if no commit is provided
+ $repo_commit = isset($matches[4])
+ ? $matches[4]
+ : $repo->head()->sha();
+
+ // Load the relevant commit
+ $commit = $repo->factory('commit', $repo_commit);
+
+ return array($repo, $repo_file, $commit);
+ }
+
+ /**
+ * Recursively searches a tree for a path, returning FALSE if is not found
+ * or an SHA-1 id if it is found.
+ *
+ * @param string $repo The repository containing the tree object
+ * @param string $tree The tree object to search
+ * @param string $path The path to search for (relative to the tree)
+ * @param int $depth The depth of the current search (for recursion)
+ *
+ * @return string|boolean The SHA-1 id of the sub-tree
+ */
+ private function tree_search($repo, $tree, $path, $depth = 0)
+ {
+ $paths = array_values(explode(DIRECTORY_SEPARATOR, $path));
+
+ $current_path = $paths[$depth];
+
+ $nodes = $tree->nodes();
+ foreach ($nodes as $node) {
+ if ($node->name() == $current_path) {
+
+ if (count($paths)-1 == $depth) {
+ // Stop, found it
+ return $node->sha();
+ }
+
+ // Recurse if necessary
+ if ($node->isDirectory()) {
+ $tree = $this->repo->factory('tree', $node->sha());
+ return $this->tree_search($repo, $tree, $path, $depth + 1);
+ }
+ }
+ }
+
+ return false;
+ }
+
+}