php/ncff.php
<?php
require_once '../www/inf/php/env.php';
define("DTLTZ", new DateTimeZone(date_default_timezone_get()));
define("DTLFMT", 'Y-m-d\tH:i:s'); # ISO 8601 date without timeZoneOffset
function toLocalTst($t) {
/* DateTime constructor uses timezone in timestring, and does not ignore it as date_create_from_format(DATE_RFC7231, $t) etc..
setTimezonew will change the time digits, so its the same moment, it even knows about summertime changes in the past years
so ->getOffset() it a method of DateTime not TimeZone!!!
*/
return (is_string($t) ? new DateTime($t) : $t)->setTimezone(DTLTZ)->format(DTLFMT);
}
function xmlPP($s) {
/* pretty print xml
each element on a separate line
intendation showing the nesting level
linefeed and intendations are inserted only before the < (tag begin char) text remains unchanged
*/
return preg_replace_callback(
'=<[/?]?|[/?]?>='
, function ($m) {
static $lv = 0, $laO = 0;
if ($m[0] ==='<') {
$laO = 1;
$r = "\n" . str_repeat(' ', $lv++);
} elseif ($m[0] ==='<?') {
$r = "\n" . str_repeat(' ', $lv);
} elseif ($m[0] ==='</') {
$lv--;
$r = $laO ? '' : "\n" . str_repeat(' ', $lv);
$laO = 0;
} else {
$r = '';
if ($m[0] ==='/>') {
$lv--;
$laO = 0;
}
}
return $r . $m[0];
}
, $s);
}
function fn2valid($f) { # in the nfts filesystem certain file names are not allowed, they are replace here by similair valid names
$r = str_replace('*', '@', trim($f, ' ')); // trailing spaces are disallowed, stars are illegal
if (str_ends_with($r, '.'))
$r[-1] = '@';
return $r === '' ? '___' : $r;
}
class NextCloud {
/*
docs: https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/index.html
*/
public $cloudId, $host, $user, $rootDir, $meta;
private $pw, $hAuth, $bUri;
public function __construct($ci, $hn, $us, $pw, $di) {
$this->cloudId = $ci;
$this->host = $hn;
$this->user = $us;
$this->pw = $pw;
$this->rootDir = $di;
$this->fPath = "/remote.php/dav/files/$di/"; # prefix in hrefs from propfind
$this->fUri = "https://$hn$this->fPath"; # dav uri for rootdir
$this->sUri = "https://$hn/remote.php/dav/";
$this->hAuth = // "WWW-Authenticate: Basic realm=\"$host\", charset=\"UTF-8\"\n"
'Authorization: Basic ' . base64_encode("$us:$pw");
}
function meta($fun, $dir) {
$now = new DateTime();
return $this->meta =[
['timestamp' , 'cloudId' , 'fun', 'dir', 'host' , 'user' , 'rootDir' , 'rootDir Uri', 'timeZone']
, [toLocalTst($now), $this->cloudId, $fun , $dir , $this->host, $this->user, $this->rootDir, $this->fUri , $now->format('e P')]
, ['path', 'id', 'type', 'owner', 'lastmod', 'size']
];
}
public function ff($dir='') {
/* find files and directories using Propfind Request of WebDav from Nextcloud using http
on host $hst in directory $dir
return the generated xml
*/
$this->meta('ff', $dir);
$ctx = stream_context_create(['http' => [
'header' => "Content-Type: application/xml\n$this->hAuth"
. "\n" . 'Depth: infinity'
, 'method' => 'PROPFIND'
, 'content' => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:fileid />
<d:resourcetype/>
<d:getcontenttype/>
<d:getlastmodified/>
<!-- oc:permissions -->
<oc:size />
<oc:owner-id />
</d:prop>
</d:propfind>
data ]]);
$r = file_get_contents("$this->fUri$dir", 0, $ctx);
// echo "ff response\n" . print_r($http_response_header, true). "\n";
if ($r == '' or $http_response_header[0] != 'HTTP/1.1 207 Multi-Status')
err("https://$hst/remote.php/dav/files/$dir/ returned $http_response_header", $http_response_header);
/* echo "\nff result\n" . preg_replace_callback('=(<d:getlastmodified>)([^<>]*)(</d:getlastmodified>)='
, fn ($m) => $m[1] . $m[2] . ' => ' . toLocalTst($m[2]) . $m[3]
, xmlpp($r)) . "\n"; */
return $r;
}
public function ffRsrc($s, $hr) {
/* parse the xml in $s returned from ff,
for each resource file/directory call $hr with an array with 6 columns
['path', 'id', 'type', 'owner', 'lastmod', 'size'] // header row
*/
$hes = function ($hh, $nm, $at) use(&$pSt, &$p1, &$pPropSt) { // handler element start
$pSt[] = $nm;
if ($nm === 'd:response')
$p1 = ['', '', '', '', '', ''];
elseif ($nm === 'd:propstat')
$pPropSt ++;
// echo "ps ele start $nm, stack " . implode(',', $pSt) . "\n";
};
$hch = function ($hh, $ch) use (&$pCh) { // handler character
$pCh .= $ch;
// echo "pc char $ch!\n";
};
$hee = function ($hh, $nm) use (&$pSt, &$p1, &$pPropSt, &$pCh, $hr) { // handler element end
$e = array_pop($pSt);
if ($nm !== $e)
err("bad pop $nm, but popped $e");
// echo "pe ele end $nm pCh $pCh\n";
if ($e === 'd:response') {
$hr($p1);
$p1 = [];
$pPropSt = 0;
} elseif ($e === 'd:prop' or $e === 'd:propstat' or $e === 'd:resourcetype' or $e === 'd:multistatus') {
} elseif ($e === 'd:status') {
if ($pPropSt === 1 ? $pCh !== 'HTTP/1.1 200 OK' : $pCh !== 'HTTP/1.1 404 Not Found')
err("d:propstat #$pPropSt status $pCh");
} elseif ($pPropSt <= 1 ) {
if ($e === 'd:collection') {
$p1[2] = $e;
} elseif ($e === 'd:href' ) {
$pa = rawurldecode($pCh);
if ('' === ($p1[0] = str_starts_with($pa, $this->fPath) ? substr($pa, strlen($this->fPath)) : "???$pa"))
$p1[0] = './';
} elseif ($e === 'd:getlastmodified' ) {
$p1[4] = toLocalTst($pCh);
} elseif (0 > $ix = $e === 'oc:fileid' ? 1 : ($e === 'd:getcontenttype' ? 2 : ($e === 'oc:owner-id' ? 3 : ($e === 'oc:size' ? 5 : - 1)))) {
echo "pe bad ele $e\n";
} else {
$p1[$ix] = $pCh;
}
}
$pCh = '';
};
$pSt = [];
$pPropSt = 0;
$pCh = '';
$xp = xml_parser_create();
xml_parser_set_option($xp, XML_OPTION_CASE_FOLDING, 0);
xml_set_element_handler($xp, $hes, $hee);
xml_set_character_data_handler($xp, $hch);
xml_parse($xp, $s, true);
}
public function ffCurl($dir='') {
/* find files and directories using Propfind Request of WebDav from Nextcloud using libcurl
on host $hst in directory $dir
return the generated xml
*/
#curl -u wa@wlkl.ch:RCisXp9HmX6Pk2flSqmWkz9w5LnOiB0xF8H0dVxIxXfefW3OFCmdCvx21H4AUNH7gQxf1188 -X GET 'https://tsueri.cloud/ocs/v1.php/cloud/users/wa@wlkl.ch' -H "OCS-
# $r = ffCurl("https://$host/remote.php/dav", $us, $pw, "$dir");
$ch = curl_init();
curl_setopt_array($ch,
[ CURLOPT_URL => "$this->fUri$dir" // => "https://cloud.hoststar.ch/remote.php/dav/files/wlkl.ch/bike"
, CURLOPT_USERNAME => $this->user
, CURLOPT_PASSWORD => $this->pw
, CURLOPT_RETURNTRANSFER => 1
, CURLOPT_VERBOSE => 1
, CURLOPT_CUSTOMREQUEST => 'PROPFIND'
, CURLOPT_HTTPHEADER => ['Depth: 99']
, CURLOPT_POSTFIELDS => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:prop>
<oc:fileid />
<d:resourcetype/>
<d:getcontenttype/>
<d:getlastmodified/>
<oc:owner-id />
<!--oc:permissions -->
<oc:size />
</d:prop>
</d:propfind>
data
]);
$r = curl_exec($ch);
curl_close($ch);
return $r;
}
public function mv($fP, $tP) {
/* move file/directory $fr to new path/name $to
*/
$fr =implode('/', array_map('rawurlencode', is_array($fP) ? $fP : explode('/', $fP)));
$to =implode('/', array_map('rawurlencode', is_array($tP) ? $tP :explode('/', $tP)));
$ctx = stream_context_create(['http' => [
'header' => "$this->hAuth\nDestination: $this->fUri$to\nOverwrite: F"
, 'method' => 'MOVE'
]]);
$r = file_get_contents("$this->fUri$fr", 0, $ctx);
if ($http_response_header[0] !== 'HTTP/1.1 201 Created')
err("mv $fr to $to failed: http_response_header", $http_response_header);
}
public function putFi($fn, $cnt, $ty='text/csv') {
// upload to host $hst a file named $fn and contents $cnt, if the file already existst, create new version of the same id
$ctx = stream_context_create(['http' => [
'header' => "Content-Type: $ty\n$this->hAuth"
, 'method' => 'PUT'
, 'content' => $cnt
]]);
$r = file_get_contents("$this->fUri$fn", 0, $ctx);
echo "putFi sent " .strlen($cnt) ." chars to $this->cloudId $this->host $fn, response $http_response_header[0]\n";
if ($http_response_header[0] !== "HTTP/1.1 201 Created" and $http_response_header[0] !== "HTTP/1.1 204 No Content")
err("putFi $fn failed, response:>n ", $http_response_header);
}
public function byId($id, $dir='') {
$ctx = stream_context_create($c = ['http' => [
'header' => "Content-Type: text/xml\n$this->hAuth"
, 'method' => 'SEARCH'
, 'content' => <<<data
<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select>
<d:prop>
<oc:fileid/>
<d:displayname/>
<d:getcontenttype/>
<d:getetag/>
<oc:size/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/$this->rootDir/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:eq>
<d:prop>
<oc:fileid/>
</d:prop>
<d:literal>$id</d:literal>
</d:eq>
</d:where>
</d:basicsearch>
</d:searchrequest>
data ]]);
$r = file_get_contents("$this->sUri", 0, $ctx);
echo "search $this->sUri response $http_response_header[0]\n";
var_dump($http_response_header);
return $r;
}
public function rename($r, $f2v, $hi) {
/* check all resources in xmmResult $r, if they need to be renamed
$f2v: callback: rename last level of path
$hi($nx, $n, $fP, $fM, $tP): callback if last level should be rename:
$nx=count, $n array for resource, $fP from path old, $fM from path for mv, $tp to path for mv
*/
$nx = -1;
$fP = $tP = [];
$hR = function ($n) use (&$nx, &$fP, &$tP, $f2v, $hi) {
$nx++;
$aS = $n[0];
if (($fo = $n[2] === 'd:collection') !== str_ends_with($aS, '/'))
err("path $aS mismatches type $n[2]");
if ($aS === './')
return;
$aP = explode('/', $fo ? substr($aS, 0, -1) : $aS);
$aV = $f2v($aL = $aP[$aY = count($aP) - 1]);
if ($aL === $aV)
return;
$y = min($aY, count($fP));
for ($x=0; $x < $y and $aP[$x] === $fP[$x]; $x++) ; # index of first difference
out("a) $x $aY fP", $fP, ", tP", $tP);
for ( ; $x < $aY ; $x++) # copy new levels
$fP[$x] = $tP[$x] = $aP[$x];
out("b) $x $aY fP", $fP, ", tP", $tP);
while (array_key_last($fP) > $aY)
array_pop($fP);
while (array_key_last($tP) > $aY)
array_pop($tP);
$fM = $tP;
$fP[$aY] = $fM[$aY] = $aL;
$tP[$aY] = $aV;
$hi($nx, $n, $fP, $fM, $tP);
};
$this->ffRsrc($r, $hR);
echo "$nx nodes in $this->cloudId\n";
}
} # end class NextCloud
const II = [ ['spzhNC', 'cloud.spzueri.ch', 'wa@wlkl.ch', 'ali{A51}sp', 'walterkeller']
, ['wlHoNC', 'cloud.hoststar.ch', 'wlkl.ch', 'ali{A51}', 'wlkl.ch']
, ['wlklNC', 'cloud.wlkl.ch', 'wa@wlkl.ch', 'p78RZ-dnXmC-3weNF-i669i-sHHQN', 'wa@wlkl.ch']
, ['keNC', 'tsueri.cloud', 'wa@wlkl.ch', 'RCisXp9HmX6Pk2flSqmWkz9w5LnOiB0xF8H0dVxIxXfefW3OFCmdCvx21H4AUNH7gQxf1188', 'wa@wlkl.ch']
];
$nc = new NextCloud(...II[2]);
outBegin($nc->cloudId);
if (0) {
$nc->mv("test2.md", " test2.md ");
$nc->mv("test3", " test3 ");
$nc->mv(" test3 /test4.md", " test3 / test4.md ");
} else if (0) {
$r = $nc->byId(1366); # spzueri 558871, wlkl 1366
echo "search result\n" . xmlPP($r);
} else if (1) {
$nc->rename($nc->ff(), 'fn2valid', function($nx, $n, $fP, $fM, $tP) use($nc) {
out("$nc->cloudId, $n[0], fP", $fP, ", tP", $tP, ", fM", $fM );
echo "$nx path $n[0] invalid, renaming " . implode('/', $fM) . " ==> " . implode('/', $tP) . "\n";
$nc->mv($fM, $tP);
});
} else if (0) {
$r = $nc->ffCurl();
echo "curl result\n" . xmlPP($r);
} else {
$r = $nc->ff();
$fo = fopen('php://memory', 'w+');
fputcsv($fo, ['}}tbh meta']);
fputcsv($fo, $nc->meta[0]);
fputcsv($fo, $nc->meta[1]);
fputcsv($fo, ['}}tbh resource']);
fputcsv($fo, $nc->meta[2]);
$nc->ffRsrc($r, fn ($r) => fputcsv($fo, $r));
echo "$nc->cloudId parsed " . strlen($r) . " bytes xml to " . ($foL = ftell($fo)). " bytes csv\n";
rewind($fo);
$nc->putFi("{$nc->cloudId}_ffid.csv", fread($fo, $foL));
fclose($fo);
echo (new DateTime())->format(DTLFMT) . " start rclone sync -v $nc->cloudId: /wkArchive/sync/$nc->cloudId\n";
$o = system("rclone sync -v $nc->cloudId: /wkArchive/sync/$nc->cloudId", $rc);
echo (new DateTime())->format(DTLFMT) . " rclone $nc->cloudId ended with rc $rc: $o \n";
}
/*
curl -u "wa@wlkl.ch:ali{A51}sp" https://cloud.spzueri.ch/remote.php/dav/ -X SEARCH -H "content-Type: text/xml" --data '<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:basicsearch>
<d:select>
<d:prop>
<oc:fileid/>
<d:displayname/>
<d:getcontenttype/>
<d:getetag/>
<oc:size/>
</d:prop>
</d:select>
<d:from>
<d:scope>
<d:href>/files/walterkeller/</d:href>
<d:depth>infinity</d:depth>
</d:scope>
</d:from>
<d:where>
<d:like>
<d:prop>
<d:getcontenttype/>
</d:prop>
<d:literal>text/%</d:literal>
</d:like>
</d:where>
<d:orderby/>
</d:basicsearch>
</d:searchrequest>'
*/
?>