nextcloud/apps/files_versioning/versionstorage.php

387 lines
12 KiB
PHP

<?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;
}
}