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