php/googleClientLib/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>'
*/
?>