<?php
//
//=========================================================
// DepotServer.php Implements a Perforce WebDAV server
//=========================================================
// +----------------------------------------------------------------------+
// | PHP Version 4 and 5 |
// | Original Name = FileSystem.php |
// +----------------------------------------------------------------------+
// | Author: Daniel Sabsay <danielsabsay@pacbell.net> |
// | |
// | Based on a model by: Hartmut Holzgraefe <hholzgra@php.net> |
// | Christian Stocker <chregu@bitflux.ch> |
// +----------------------------------------------------------------------+
//
require_once realpath('.').'/DAVServer.php';
define ('CR',"\n");
define ('DIRR','httpd/unix-directory');
define ('DIRP','DavDirectoryPlaceholder');
// prohibited characters in file names: @ %40, # %23, * %2A, % %25
/**
* Perforce depot access using local p4 client
*
* @access public
*/
class Depot_Filesystem extends HTTP_WebDAV_Server
{
/**
* Root directory for WebDAV access
*
* @access private
* @var string
*/
var $base = "";
/**
* Logging control: 0=none, 1=connection, 2=detail 3=debug
*
* @access private
* @var integer
*/
var $logging = 0;
/**
* Perforce Host connection parameters
*
* @access private
* @var string
*/
var $depot_host = 'public.perforce.com';
var $depot_port = '1666';
var $depot_agent = '/usr/local/bin/p4';
/**
* Perforce user & password
*
* @access private
* @var string
*/
var $depot_user;
var $depot_passwd;
/**
* Perforce Admin user & password
*
* Used ONLY for the removal of placeholder files
* that force new directories to be "visible"
*
* @access private
* @var string
*/
var $admin_user;
var $admin_passwd;
/**
* Serve a webdav request
*
* @access public
* @param string
*/
function ServeRequest($method)
{
// let the base class do all the work
parent::ServeRequest($method);
}
/**
* PERFORCE USER AUTHORIZATION
*
* @access private
* @return bool true on successful authentication
*/
function _check_auth()
{
if ($this->logging>2) {
error_log('**Perforce P4 version='.$this->sp4cmd('-V','',''));
}
if (empty($_SERVER['PHP_AUTH_USER'])) return false;
// Authenticate with the Perforce repository server
$this->depot_user = $_SERVER['PHP_AUTH_USER'];
$this->depot_passwd = $_SERVER['PHP_AUTH_PW'];
$r = $this->sp4cmd('user','-o','');
$p = strpos($r,'Password:'); // skip the pro-forma password
// This second password label only appears if login was
// accepted by the Perforce repository.
if (strpos(substr($r,$p+9),'Password:')) return true;
$p4 = strpos($this->sp4cmd('-V','',''),'Perforce Software');
if ($p4===false) error_log('**** P4 client module not found at '.$this->depot_agent);
if ($this->logging>1) error_log('**Login error, user='.$this->depot_user);
return false;
} // end _check_auth
/**
* PROPFIND method handler
*
* @param array general parameter passing array
* @param array return array for file properties
* @return bool true on success
*/
function PROPFIND(&$options, &$files)
{
$path = $options['path'];
$p4path = '/'.$path;
$datePat = array('date'=>'time ');
$dirPat = array('path'=>'dir /');
$depotPat = array('raw'=>'Depot ');
$filePat = array('path'=>'depotFile /',
'size'=>'fileSize ',
'date'=>'headTime ');
if ($p4path=='//') {
$group['self'] = array(array('path'=>''));
if (!empty($options['depth'])) { // skip if self only (depth==0)
$group['depots'] = $this->p4list('depots','',$depotPat);
}
} else {
if (substr($path,-1)=='/') {
$is_dir = true; // Assume it's a directory
$path = substr($path,0,strlen($path)-1); // drop trailing separator
$p4path = '/'.$path;
} else { // else might be a file
$self_file = $this->p4list('fstat -Ol',$p4path,$filePat);
if (empty($self_file)) { $is_dir = true; }
else { $group['files'] = $self_file[0]; }
}
if ($is_dir) { // Not a file, probably a directory
$c_dirs = count($this->p4list('dirs -D',$p4path,$dirPat)); // dir exists?
$c_dirs += count($this->p4list('depots','',array('x'=>'Depot '.basename($path))));
if (!$c_dirs) { return false; } // none of the above, then doesn't exist
if (strpos($options['depth'],'noroot')===false) { // Omit self ?
$group['self'] = array(array('path'=>$path));
}
if (!empty($options['depth'])) { // skip if self only (depth==0)
$group['dirs']=$this->p4list('dirs -D',$p4path.'/*',$dirPat);
// Deleted files are not shown because they do not exhibit a filesize
// attribute with the "Ol" option, and are therefore ignored by p4list.
$group['files'] = $this->p4list('fstat -Ol',$p4path.'/*',$filePat);
//$group['config']=array($path.'/**my_configuration**');
}
}
}
$files['files'] = array(); // initialize item stack
foreach ($group as $group_type=>$children) {
foreach ($children as $child) {
switch ($group_type) {
case 'depots':
$child['path'] = '/'.substr($child['raw'],0,strpos($child['raw'],' '));
// fall thru
case 'dirs':
$p4subDirs = '/'.$child['path'].'/*';
$lastMod = $this->p4list('changes -m1',$p4subDirs,$datePat);
$child['date'] = $lastMod[0]['date'];
// fall thru
case 'self':
$child['name'] = basename($child['path']);
$child['path'] .= '/'; // sub-directories get trailing separator
$child['rez'] = 'collection';
$child['type'] = DIRR;
break;
case 'files':
$child['name'] = basename($child['path']);
$child['type'] = $this->_mimetype($child['path']);
}
if ($child['name'] == DIRP) continue; // ignore any placeholder leakage
$props = array(); // initialize property array
array_push($props,$this->mkprop('getcontentlength',$child['size']));
array_push($props,$this->mkprop('resourcetype',$child['rez']));
array_push($props,$this->mkprop('getcontenttype',$child['type']));
array_push($props,$this->mkprop('displayname',$child['name']));
array_push($props,$this->mkprop('getlastmodified',$child['date']));
array_push($props,$this->mkprop('creationdate',0));
// Summarize properties for this item
array_push($files['files'],array('path'=>$child['path'],'props'=>$props));
}
}
return true;
} // end PROPFIND
/**
* try to detect the mime type of a file
*
* @param string file path
* @return string guessed mime type
*/
function _mimetype($path)
{
// Fallback solution: try to guess the type by the file extension
// TODO: add more ...
// TODO: it has been suggested to delegate mimetype detection
// to apache but this has at least three issues:
// - works only with apache
// - needs file to be within the document tree
// - requires apache mod_magic
// TODO: can we use the registry for this on Windows?
// OTOH if the server is Windos the clients are likely to
// be Windows, too, and tend do ignore the Content-Type
// anyway (overriding it with information taken from
// the registry)
// TODO: have a seperate PEAR class for mimetype detection?
switch (strtolower(strrchr(basename($path), "."))) {
case ".html":
$mime_type = "text/html";
break;
case ".gif":
$mime_type = "image/gif";
break;
case ".jpg":
$mime_type = "image/jpeg";
break;
case ".png":
$mime_type = "image/png";
break;
case ".txt":
$mime_type = "text/plain";
break;
case ".xml":
$mime_type = "text/xml";
break;
case ".php":
$mime_type = "text/php";
break;
default:
$mime_type = "application/octet-stream";
break;
}
return $mime_type;
}
/**
* GET method handler
*
* @param array parameter passing array
* @return bool true on success
*/
function GET(&$options)
{
$path = $options['path'];
$p4dir = '/'.dirname($path);
$node = basename($path);
$p4path = $p4dir.'/'.$node; // drop possible trailing separator
$fileAPat = array('path'=>'depotFile /','action'=>'action ','date'=>'time ');
$exists = $this->p4list('files',$p4path,$fileAPat);
if (empty($exists)) return false;
if ($exists[0]['action']=='delete') return false;
$mod_date = $exists[0]['date'];
header('Accept-Ranges: bytes');
header('ETag: "'.uniqid('E').'"');
$mod_since = $hdrs['If-Modified-Since'];
if ($mod_since) {
if (strtotime($mod_since) >= $mod_date) return '304 Not Modified';
}
$file = $this->base.uniqid('F'); // Generate temporary filename
$this->sp4cmd('print -q -o '.$file,'"'.$p4path.'"',''); // read data from depot
$options['mimetype'] = $this->_mimetype($path);
$options['mtime'] = $mod_date;
$options['size'] = filesize($file);
$fr = fopen($file,'r');
$fail = !$fr;
if ($fail) {
error_log('*** Filesystem permission error ***');
unlink($file);
} else {
$options['stream'] = $fr; // Open stream to temp file
$options['delete_path'] = $file; // Mark temp for disposal
}
return ($fail)? false:true;
} // end GET
function HEAD(&$params)
{
$status = $this->GET($params);
if ($status===false) return false;
header('Content-Length: '.$params['size']);
fclose($params['stream']);
unlink($params['delete_path']);
return $status;
} // end HEAD
/**
* PUT method handler
*
* @param array parameter passing array
* @return bool true on success
*/
function PUT(&$options)
{
$path = $options['path'];
$p4dir = '/'.dirname($path);
$node = basename($path);
$p4path = $p4dir.'/'.$node; // drop possible trailing separator
if ($p4path=='//') return '403 Forbidden, cannot make depot';
$uid = uniqid('');
$ws_id = 'W'.$uid;
$dir = $this->base.'D'.$uid; // temp filesystem directory name
// Create a temporary private directory on the server
$fail = (mkdir($dir)===false); // make temp directory
if (!$fail) { // connect to HTTP data stream
$fr = $options['stream'];
$fail = ($fr===false);
}
if (!$fail) { // open temp file for data
$file = $dir.'/'.$node;
$fw = fopen($file,'w');
$fail = ($fw===false);
}
if (!$fail) { // Copy file contents from the user's HTTP request
while (!feof($fr)) fwrite($fw,fread($fr,4096));
fclose($fr); fclose($fw);
}
if (!$fail) $fail = !$this->p4work($ws_id,$p4dir,$dir); // new workspace
if (!$fail) $this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id);
if (!$fail) { // open for EDIT or ADD depending on current existence
$filePatA = array('path'=>'depotFile /','action'=>'action ');
$exists = $this->p4list('files',$p4path,$filePatA);
if (empty($exists)) { $update = 'add'; }
else { $update = ($exists[0]['action']=='delete')? 'add':'edit'; }
$x = $this->sp4cmd($update,'"'.$p4path.'"','-c '.$ws_id);
$fail = (strpos($x,'opened for')===false);
if ($fail) error_log('***'.$update.' '.$x);
} // SUBMIT the change
if (!$fail) $fail = !$this->p4submit($ws_id,$update,$p4path);
$this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
if (!$fail) {
if (!strpos($this->sp4cmd('files',$p4dir.'/'.DIRP,''),'no such')) {
$this->p4faux_drop($p4dir); // remove faux dir placeholder if present
}
}
unlink($file); // delete temp file
rmdir($dir); // delete temp directory
return ($fail)? false:true;
} // end PUT
/**
* MKCOL method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function MKCOL($options)
{
$path = $options['path'];
$p4dir = '/'.dirname($path);
$p4path = $p4dir.'/'.basename($path); // drop possible trailing separator
$uid = uniqid('');
$ws_id = 'W'.$uid;
$dir = $this->base.'D'.$uid; // temp filesystem directory name
if ($p4path=='//') return '403 Forbidden, cannot make depot';
// Parent directory must already exist
if (count(split('/',$path))>3) { // Regular directory or depot?
$exists = $this->p4list('dirs -D',$p4dir,array('x'=>'dir /'));
} else {
$exists = $this->p4list('depots','',array('x'=>'Depot '.substr(dirname($path),1)));
}
if (empty($exists)) return '403 Forbidden, no parent directory';
// Directory with the same name must not already exist
$exists = $this->p4list('files',$p4path,array('path'=>'dir /'));
if (!empty($exists)) return '405 Not allowed, directory already exists';
// File with the same name must not already exist
$exists = $this->p4list('files',$p4path,array('path'=>'depotFile /'));
if (!empty($exists)) return '409 Conflict, file already exists';
if (!empty($_SERVER['CONTENT_LENGTH'])) { // no body parsing yet
return '415 Unsupported media type';
}
// Create a persistent empty (faux) directory by putting
// a "deleted" placeholder file inside.
$fail = !$this->p4faux_make($p4path);
return ($fail)? false:'201 Created';
} // end MKCOL
/**
* DELETE method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function DELETE($options)
{
$path = $options['path'];
$p4dir = '/'.dirname($path);
if ($p4dir=='//') return '403 Forbidden, cannot delete depot.';
$node = basename($path);
$p4path = $p4dir.'/'.$node;
// Create a temporary private directory in our filespace
$uid = uniqid('');
$ws_id = 'W'.$uid;
$dir = $this->base.'D'.$uid; // temp filesystem directory name
$fail = !mkdir($dir); // make temp directory
if (!$fail) {
$file = $dir.'/'.$node; // create temporary sacrificial
$fail = !touch($file); // file that P4 can delete
}
if (!$fail) { // Is item to delete an empty (faux) directory?
if (!strpos($this->sp4cmd('files',$p4path.'/'.DIRP,''),'no such')) {
$this->p4faux_drop($p4path); // yes, delete by removing placeholder
} else { // no, must delete the actual file [or directory]
$fail = !$this->p4work($ws_id,$p4dir,$dir); // attach workspace
if (!$fail) {
$this->sp4cmd('flush','"'.$p4path.'"','-c '.$ws_id);
$x = $this->sp4cmd('delete','"'.$p4path.'"','-c '.$ws_id);
$fail = (strpos($x,'opened for')===false);
}
if ($fail) error_log('***delete '.$x.$p4path);
if (!$fail) $fail = !$this->p4submit($ws_id,'delete',$p4path);
}
}
$this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
unlink($file); // delete temp file (should be redundant)
rmdir($dir); // delete temp directory
return ($fail)? '404 Not found-3':true;
} // end DELETE
/**
* MOVE method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function MOVE($options)
{
return $this->COPY($options, true);
} // end MOVE
/**
* COPY method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function COPY($options, $del=false)
{
// TODO Property updates still broken (Litmus should detect this?)
$fileAPat = array('path'=>'depotFile /','action'=>'action ');
if (!empty($_SERVER['CONTENT_LENGTH'])) { // no body parsing yet
return '415 Unsupported media type';
}
// no copying to different WebDAV Servers yet
if (isset($options['dest_url'])) {
return '502 bad gateway';
}
$path = $options['path'];
$p4dir = '/'.dirname($path);
$node = basename($path);
$p4src = $p4dir.'/'.$node; // drop possible trailing separator
$fileAPat = array('path'=>'depotFile /','action'=>'action ');
$file = $this->p4list('files',$p4src,$fileAPat);
$dir = $this->p4list('dirs -D',$p4src,array('x'=>'dir '));
$depot = $this->p4list('depots',$p4src,array('x'=>'Depot '));
if (empty($file)&&empty($dir)&&empty($depot)) { return '404 Not found-1'; }
else { if ($file[0]['action']=='delete') return '404 Not found-2'; }
$dest = $options['dest'];
$p4destDir = '/'.dirname($dest);
$node = basename($dest);
$p4dest = $p4destDir.'/'.$node; // drop possible trailing separator
$exists = $this->p4list('files',$p4dest,$fileAPat);
if (empty($exists)) { $new = true; }
else { $new = ($exists[0]['action']=='delete')? true:false; }
$dir = $this->p4list('dirs -D',$p4dest,array('x'=>'dir '));
$existing_col = false;
if (!$new) {
if ($del && !empty($dir)) {
if (!$options['overwrite']) return '412 precondition failed';
$existing_col = true;
}
}
if (!$new) {
if ($options['overwrite']) {
$stat = $this->DELETE(array('path'=>$dest));
if (substr($stat,0,1) != '2') return $stat;
} else {
return '412 precondition failed';
}
}
// Is the source a directory?
$c_dir = count($this->p4list('dirs -D',$p4src,array('x'=>'dir ')));
$c_dir += count($this->p4list('depots',$p4src,array('x'=>'Depot ')));
if ($c_dir) {
$dx = '/...';
// RFC 2518 Section 9.2, last paragraph
if ($options['depth'] != 'infinity') {
error_log('--depth='.$options['depth']);
return '400 Bad request';
}
} else {
$dx = '';
}
// Create a temporary private directory on the server
$uid = uniqid('');
$ws_id = 'W'.$uid;
$dir = $this->base.'D'.$uid; // temp filesystem directory name
$fail = !mkdir($dir); // make temp directory
$fail = !$this->p4work($ws_id,$p4destDir,$dir); // attach a workspace
if (!$fail) {
$this->sp4cmd('flush','"'.$p4dest.'"','-c '.$ws_id);
$x = $this->sp4cmd('integ -f -i -d','"'.$p4src.$dx.'" "'.$p4dest.$dx.'"','-c '.$ws_id);
$fail = (strpos($x,'branch/sync')===false);
if ($fail) error_log('***integrate '.$x);
}
if (!$fail) {
$x = $this->sp4cmd('resolve -at','"'.$p4dest.$dx.'"','-c '.$ws_id);
$fail = (strpos($x,'resolve')===false);
if ($fail) error_log('***resolve '.$x);
}
if (!$fail) $fail = !$this->p4submit($ws_id,'copy of '.$p4src,$p4dest);
$this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
// Check if this makes a faux directory non-empty
if (!strpos($this->sp4cmd('files',$p4destDir.'/'.DIRP,''),'no such')) {
$this->p4faux_drop($p4destDir); // yes, remove unnecessary placeholder
}
rmdir($dir); // delete temp directory
if (!$fail) {
if ($del) {
$stat = $this->DELETE(array('path'=>$path));
if (substr($stat,0,1) != '2') return $stat;
}
}
return ($new && !$existing_col) ? "201 Created" : "204 No Content";
} // end COPY
/**
* PROPPATCH method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function PROPPATCH(&$options)
{
foreach($options['props'] as $key => $prop) {
if($ns == 'DAV:') {
$options['props'][$key]['status'] = '403 Forbidden';
}
}
return '';
}
/**
* LOCK method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function LOCK(&$options)
{
if(isset($options['update'])) { // Lock Update
}
$options['timeout'] = time()+1; // 1 second hardcoded
return '200 OK';
}
/**
* UNLOCK method handler
*
* @param array general parameter passing array
* @return bool true on success
*/
function UNLOCK(&$options)
{
return "200 OK";
}
/**
* checkLock() helper
*
* @param string resource path to check for locks
* @return bool true on success
*/
function checkLock($path)
{
// TODO
return false;
}
/**
* p4faux_make private method
*
* Create a persistent placeholder file that makes a persistent
* empty (faux) directory visible.
*
* @param string workspace name
* @param string Perforce directory path
*/
function p4faux_make($p4dir) {
$uid = uniqid('');
$ws_id = 'W'.$uid;
$dir = $this->base.'D'.$uid; // temp filesystem directory name
$dummy = $dir.'/'.DIRP;
$p4dummy = $p4dir.'/'.DIRP;
$fail = !mkdir($dir);
if (!$fail) touch($dummy); // make empty place-holder file
$fail = !$this->p4work($ws_id,$p4dir,$dir); // attach workspace
if (!$fail) {
$this->sp4cmd('flush','"'.$p4dir.'"','-c '.$ws_id);
$x = $this->sp4cmd('add','"'.$p4dummy.'"','-c '.$ws_id);
$fail = (strpos($x,'assuming')===false);
if (!$fail) $fail = !$this->p4submit($ws_id,'make directory',$p4dummy);
}
if (!$fail) {
$this->sp4cmd('flush','"'.$p4dir.'"','-c '.$ws_id);
$x = $this->sp4cmd('delete','"'.$p4dummy.'"','-c '.$ws_id);
$fail = (strpos($x,'opened')===false);
}
if (!$fail) $fail = !$this->p4submit($ws_id,'delete directory placeholder',$p4dummy);
$this->sp4cmd('client','-d -f '.$ws_id,''); // release workspace
unlink($dummy); // delete temp file (should be redundant)
rmdir($dir);
return ($fail)? false:true;
}
function p4faux_drop($p4dir) { // Remove empty directory dummy placeholder file.
$save_user = $this->depot_user; // save normal user
$save_passwd = $this->depot_passwd; // save normal password
$this->depot_user = $this->admin_user; // substitute Perforce superuser
$this->depot_passwd = $this->admin_passwd; // and superuser password
$this->sp4cmd('obliterate -y','"'.$p4dir.'/'.DIRP.'"',''); // remove placeholder
$this->depot_user = $save_user; // restore normal user
$this->depot_passwd = $save_passwd; // restore normal password
}
function p4submit($ws_id,$action,$p4path) { // SUBMIT change
$change = 'Change: new'.CR;
$change .= 'Description:'.CR;
$change .= ' DAV-'.$action.' by p4DAV/'.$this->depot_user.CR;
$change .= 'Files:'.CR;
$change .= ' '.$p4path;
$x=shell_exec('echo \''.$change.'\' | '.$this->p4cmd('change -i','','-c '.$ws_id));
$fail = (strpos($x,'created with')===false);
if ($fail) { error_log('***change '.$x.' '.$change); }
else {
$cl_id = substr(substr($x,0,strpos($x,' created')),7);
$x = $this->sp4cmd('submit','-c '.$cl_id,'-c '.$ws_id);
$fail = (strpos($x,'submitted')===false);
if ($fail) error_log('***submit '.$x);
}
return ($fail)? false:true;
}
function p4work($ws_id,$p4dir,$fsdir) {
$view = 'Client: '.$ws_id.CR;
$view .= 'Owner: '.$this->depot_user.CR;
$view .= 'Options: allwrite noclobber nocompress unlocked nomodtime normdir'.CR;
$view .= 'LineEnd: local'.CR;
$view .= 'Root: '.$fsdir.CR;
$view .= 'View: '.CR;
$view .= ' "'.$p4dir.'/..." "//'.$ws_id.'/..."';
$x = shell_exec('echo \''.$view.'\' | '.$this->p4cmd('client -i','',''));
$fail = (strpos($x,'saved')===false);
if ($fail) error_log('***workspace '.$x.' '.$view);
return ($fail)? false:true;
}
function p4cmd($cmd,$path,$gopts) {
$p4cmd = $this->depot_agent.' '.$gopts;
$p4cmd .=' -u '.$this->depot_user;
$p4cmd .=' -P '.$this->depot_passwd;
$p4cmd .=' -p '.$this->depot_host.':'.$this->depot_port;
$p4cmd .=' '.$cmd.' '.$path.' 2>&1';
return $p4cmd;
}
function p4list($cmd,$path,$filters) {
$quoted_path = (empty($path))? '':'"'.$path.'"';
$data_lines = split(CR,$this->sp4cmd($cmd,$quoted_path,'-ztag'));
$responses = array();
$filter_count = count($filters);
$hits = 0;
$matches = array();
foreach ($data_lines as $item) {
if ($this->logging>2) error_log('**'.$item);
if (empty($item)) { $hits=0; $matches=array(); continue; }
foreach ($filters as $key=>$pattern) {
$skip = strlen($pattern);
$pos = strpos($item,$pattern);
if ($pos===false) continue;
$hits++;
$matches[$key]=trim(substr($item,$pos+$skip));
}
if ($filter_count<=$hits) { $hits=0; array_push($responses,$matches); }
}
return $responses;
}
function sp4cmd($a,$b,$c) { return shell_exec($this->p4cmd($a,$b,$c)); }
}
?> | # | Change | User | Description | Committed | |
|---|---|---|---|---|---|
| #28 | 4801 | Daniel Sabsay | Copyright and default user name cleaned up | ||
| #27 | 4800 | Daniel Sabsay |
Final documentation update GET method updated Log now reports depot host & port being served |
||
| #26 | 4798 | Daniel Sabsay | Add logging P4 version to server log if "debug-level" logging | ||
| #25 | 4797 | Daniel Sabsay | Fix bug in MKCOL method | ||
| #24 | 4794 | Daniel Sabsay |
Fix bugs in MKCOL, COPY, DELETE & PUT Restore full authentication checking |
||
| #23 | 4792 | Daniel Sabsay | Refinements and bug fixes to MOVE method | ||
| #22 | 4779 | Daniel Sabsay | A few bugs chased | ||
| #21 | 4778 | Daniel Sabsay |
Improvements in logging (now shows response headers) Change in directory searching mechanism Now appropriately handles If-Modified-Since headers |
||
| #20 | 4777 | Daniel Sabsay | adjustments to MOVE & DELETE | ||
| #19 | 4775 | Daniel Sabsay | Fix PUT method. | ||
| #18 | 4774 | Daniel Sabsay | Repackaged functions all working | ||
| #17 | 4764 | Daniel Sabsay | MOVE now works for a single file | ||
| #16 | 4752 | Daniel Sabsay | First cut at MOVE & COPY methods | ||
| #15 | 4750 | Daniel Sabsay |
Mac Finder can now mount the root (repository) level. Directory last-modified dates are now retrieved. Full log implementation now shows both sides of dialog. |
||
| #14 | 4749 | Daniel Sabsay |
The code now uses no persistant workspaces or changelists. Switched the faux directory mechanism to use deleted placeholder files. Switched the logging mechanism to use the webserver logging methods. |
||
| #13 | 4748 | Daniel Sabsay | Fix a few remaining bugs in PROPFIND and remove debugging code from PUT | ||
| #12 | 4747 | Daniel Sabsay | PROPFIND method refinements | ||
| #11 | 4746 | Daniel Sabsay | Refinements to PUT & MKCOL | ||
| #10 | 4744 | Daniel Sabsay |
Fix PUT so it overwrites a deleted file Improve MKCOL logic |
||
| #9 | 4737 | Daniel Sabsay | Initial working version of MKCOL method | ||
| #8 | 4731 | Daniel Sabsay | Re-code faux directory logic | ||
| #7 | 4729 | Daniel Sabsay | Add new logic to handle disposal of faux directory files at end of PUT command | ||
| #6 | 4728 | Daniel Sabsay | Update descriptive comments. | ||
| #5 | 4727 | Daniel Sabsay |
Improve PUT functioning. Package workspace & change specs into functions. |
||
| #4 | 4726 | Daniel Sabsay | Dates and File sizes now work, added -D flag to DIRS commands. | ||
| #3 | 4725 | Daniel Sabsay | Changes to the DELETE and LIST logic | ||
| #2 | 4716 | Daniel Sabsay | fix mispelled PUT return value | ||
| #1 | 4711 | Daniel Sabsay | PUT works for first time |