parent
4ea927a798
commit
69a2173fdb
@ -1,12 +0,0 @@ |
||||
<?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)); |
||||
@ -1,14 +0,0 @@ |
||||
<?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(); |
||||
} |
||||
@ -1,20 +0,0 @@ |
||||
<?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'); |
||||
@ -1,12 +0,0 @@ |
||||
<?xml version="1.0"?> |
||||
<info> |
||||
<id>files_versioning</id> |
||||
<name>Versioning and Backup</name> |
||||
<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> |
||||
<types> |
||||
<filesystem/> |
||||
</types> |
||||
</info> |
||||
@ -1 +0,0 @@ |
||||
1.0.0 |
||||
@ -1,3 +0,0 @@ |
||||
#file_versioning_commit_chzn { |
||||
width: 15em; |
||||
} |
||||
@ -1,25 +0,0 @@ |
||||
$(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'); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,12 +0,0 @@ |
||||
<?php |
||||
|
||||
require_once('granite/git/blob.php'); |
||||
require_once('granite/git/commit.php'); |
||||
require_once('granite/git/repository.php'); |
||||
require_once('granite/git/tag.php'); |
||||
require_once('granite/git/tree.php'); |
||||
require_once('granite/git/tree/node.php'); |
||||
require_once('granite/git/object/index.php'); |
||||
require_once('granite/git/object/raw.php'); |
||||
require_once('granite/git/object/loose.php'); |
||||
require_once('granite/git/object/packed.php'); |
||||
@ -1,34 +0,0 @@ |
||||
<?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(); |
||||
@ -1,12 +0,0 @@ |
||||
<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> |
||||
@ -1,386 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
@ -1,686 +0,0 @@ |
||||
<?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; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue