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