######################################################################################################################## # $Id$ ######################################################################################################################### # 50_SSFile.pm # # (c) 2020-2021 by Heiko Maaz # e-mail: Heiko dot Maaz at t-online dot de # # This Module integrate the Synology File Station into FHEM # # This script is part of fhem. # # Fhem is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # # Fhem is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with fhem. If not, see . # ######################################################################################################################### # # Definition: define SSFile [ServerPort] [Protocol] # # Example: define SynFile SSFile 192.168.2.20 [5000] [HTTP(S)] # package FHEM::SSFile; ## no critic 'package' use strict; use warnings; use utf8; eval "use JSON;1;" or my $SSFileMM = "JSON"; ## no critic 'eval' # Debian: apt-get install libjson-perl use Data::Dumper; # Perl Core module use GPUtils qw(GP_Import GP_Export); # wird für den Import der FHEM Funktionen aus der fhem.pl benötigt use FHEM::SynoModules::API qw(apistatic); # API Modul use FHEM::SynoModules::SMUtils qw( completeAPI showAPIinfo showModuleInfo addSendqueue listSendqueue purgeSendqueue checkSendRetry startFunctionDelayed evaljson smUrlEncode getClHash delClHash delReadings setCredentials getCredentials showStoredCredentials setReadingErrorState setReadingErrorNone login logout moduleVersion trim slurpFile jboolmap ); # Hilfsroutinen Modul use FHEM::SynoModules::ErrCodes qw(expErrors); # Error Code Modul use MIME::Base64; use POSIX qw(strftime); use Time::HiRes qw(gettimeofday); use HttpUtils; use Encode; use Encode::Guess; use File::Find; use File::Glob ':bsd_glob'; no if $] >= 5.017011, warnings => 'experimental::smartmatch'; eval "use FHEM::Meta;1" or my $modMetaAbsent = 1; ## no critic 'eval' # no if $] >= 5.017011, warnings => 'experimental'; # Run before module compilation BEGIN { # Import from main:: GP_Import( qw( attr AttrVal BlockingCall BlockingKill BlockingInformParent CancelDelayedShutdown CommandSet CommandAttr CommandDelete CommandDefine CommandGet CommandSetReading CommandTrigger Debug data defs devspec2array FileWrite FileDelete FileRead FmtTime FmtDateTime fhemTimeLocal HttpUtils_NonblockingGet init_done InternalTimer IsDisabled Log3 modules parseParams readingFnAttributes ReadingsVal RemoveInternalTimer ResolveDateWildcards readingsBeginUpdate readingsBulkUpdate readingsBulkUpdateIfChanged readingsEndUpdate readingsSingleUpdate setKeyValue urlEncode urlDecode ) ); # Export to main context with different name # my $pkg = caller(0); # my $main = $pkg; # $main =~ s/^(?:.+::)?([^:]+)$/main::$1\_/gx; # foreach (@_) { # *{ $main . $_ } = *{ $pkg . '::' . $_ }; # } GP_Export( qw( Initialize ) ); } # Versions History intern my %vNotesIntern = ( "1.0.1" => "18.07.2021 compatibility to DSM7 ", "1.0.0" => "25.05.2021 ready for check in ", "0.8.1" => "24.05.2021 fix FHEM crash when malfomed JSON is received ". "Forum: https://forum.fhem.de/index.php/topic,115371.msg1158531.html#msg1158531 ", "0.8.0" => "18.03.2021 extend commandref, switch to 'stable' ", "0.7.7" => "07.01.2021 avoid FHEM crash if Cache file content is not valid JSON format ", "0.7.6" => "20.12.2020 minor change to avoid increase memory ", "0.7.5" => "07.12.2020 minor fix avoid overtakers ", "0.7.4" => "30.11.2020 add mtime, crtime to uploaded files ", "0.7.3" => "29.11.2020 fix (prepare)Download without dest= option", "0.7.2" => "22.11.2020 undef variables containing a lot of data in execOp ", "0.7.1" => "08.11.2020 fix download, fix perl warning while upload not existing files ", "0.7.0" => "02.11.2020 new set command deleteRemoteObj, fix download object with space in name ", "0.6.0" => "30.10.2020 Upload files may contain wildcards *. ", "0.5.0" => "26.10.2020 new Setter Upload and fillup upload queue asynchronously, some more improvements around Upload ", "0.4.0" => "18.10.2020 add reqtype to addSendqueue, new Setter prepareDownload ", "0.3.0" => "16.10.2020 create Reading Hash instead of Array ", "0.2.0" => "16.10.2020 some changes in subroutines ", "0.1.0" => "12.10.2020 initial " ); my %hset = ( # Hash für Set-Funktion (needcred => 1: Funktion benötigt gesetzte Credentials) credentials => { fn => \&_setcredentials, needcred => 0 }, eraseReadings => { fn => \&_seteraseReadings, needcred => 0 }, listQueue => { fn => \&listSendqueue, needcred => 0 }, logout => { fn => \&_setlogout, needcred => 0 }, purgeQueue => { fn => \&purgeSendqueue, needcred => 0 }, startQueue => { fn => \&_setstartQueue, needcred => 1 }, Download => { fn => \&_setDownload, needcred => 1 }, prepareDownload => { fn => \&_setDownload, needcred => 1 }, Upload => { fn => \&_setUpload, needcred => 1 }, prepareUpload => { fn => \&_setUpload, needcred => 1 }, listUploadsDone => { fn => \&_setlistUploadsDone, needcred => 0 }, deleteUploadsDone => { fn => \&_setdeleteUploadsDone, needcred => 0 }, deleteRemoteObject => { fn => \&_setdeleteRemoteObject, needcred => 1 }, ); my %hget = ( # Hash für Get-Funktion (needcred => 1: Funktion benötigt gesetzte Credentials) apiInfo => { fn => \&_getapiInfo, needcred => 1 }, backgroundTaskList => { fn => \&_getbackgroundTaskList, needcred => 1 }, fileStationInfo => { fn => \&_getfilestationInfo, needcred => 1 }, remoteFileInfo => { fn => \&_getremoteFileInfo, needcred => 1 }, remoteFolderList => { fn => \&_getRemoteFolderList, needcred => 1 }, storedCredentials => { fn => \&_getstoredCredentials, needcred => 1 }, versionNotes => { fn => \&_getversionNotes, needcred => 0 }, ); my %hmodep = ( # Hash für Opmode Parser. fileStationInfo => { fn => \&_parsefilestationInfo, doevt => 1 }, # doevt: 1 - Events dürfen ausgelöste werden. 0 - keine Events backgroundTask => { fn => \&_parsebackgroundTaskList, doevt => 1 }, shareList => { fn => \&_parseFiFo, doevt => 1 }, remoteFolderList => { fn => \&_parseFiFo, doevt => 0 }, remoteFileInfo => { fn => \&_parseFiFo, doevt => 1 }, download => { fn => \&_parseDownload, doevt => 1 }, upload => { fn => \&_parseUpload, doevt => 1 }, deleteRemoteObj => { fn => \&_parsedeleteRemoteObject, doevt => 1 }, ); my %hvada = ( # Funktionshash Version Adaption "a01" => {AUTH => "6", LIST => "1", UPLOAD => "2" }, ); # Versions History extern my %vNotesExtern = ( "0.6.0" => "30.10.2020 A new Set command Upload is integrated and the fillup upload queue routine is running asynchronously.
". "Some more improvements around Upload were done, e.g. Upload files may contain wildcards *.", "0.1.0" => "12.10.2020 initial " ); # Hints EN my %vHintsExt_en = ( "2" => "When defining the upload target paths POSIX %-Wildcards can be used as part of the target path. ". "This way changing upload targets can be created depending on the current timestamp.
". "Examples of prominent wildcards:

". "". "". "". "". "". "". "". "". "". "". "". "". "". "". "
Specification replaced by Example
%%a The abbreviated weekday name according to the current locale Thu
%y Year, last two digits (00-99) 01
%Y Year incl. century 2020
%m Month as decimal number (01-12) 08
%%d The day of the month as decimal number (01-31) 23
%H The hour in 24-hour format (00-23) 14
%M The minute as decimal number (00-59) 55
%S The second as decimal number (00-60) 02
%V The ISO 8601 week number of the current year as decimal number (01-53) 34
%T The time in 24-hour notation (%H:%M:%S) 14:55:02
". "
" , "1" => "The module integrates Synology File Station with FHEM. " ); # Hints DE my %vHintsExt_de = ( "2" => encode ("utf8", "Bei der Definition der Upload Zielpfade könnnen POSIX %-Wildcards als Bestandteil des Zielpfads verwendet werden. ". "Damit können wechselnde Uploadziele in Abhängigkeit des aktuellen Timestamps erzeugt werden.
". "Beispiele prominenter Wildcards:

". "". "". "". "". "". "". "". "". "". "". "". "". "". "". "
Spezifizierung ersetzt durch Beispiel
%%a Der abgekürzte Wochentagsname entsprechend dem aktuellen Gebietsschema Mo
%y Jahr, letzte zwei Ziffern (00-99) 01
%Y Jahr incl. Jahrhundert 2020
%m Monat als Dezimalzahl (01-12) 08
%%d Der Tag des Monats als Dezimalzahl (01-31) 23
%H Die Stunde im 24-Stunden-Format (00-23) 14
%M Die Minute als Dezimalzahl (00-59) 55
%S Die Sekunde als Dezimalzahl (00-60) 02
%V Die ISO 8601-Wochennummer des laufenden Jahres als Dezimalzahl (01-53) 34
%T Die Uhrzeit in 24-Stunden-Notation (%H:%M:%S) 14:55:02
". "
" ), "1" => "Das Modul integriert die Synology File Station in FHEM. " ); # Standardvariablen my $splitstr = "!_ESC_!"; # Split-String zur Übergabe in getCredentials, login & Co. my $queueStartFn = "FHEM::SSFile::getApiSites"; # Startfunktion zur Queue-Abarbeitung my $mbf = 1048576; # Divisionsfaktor für Megabytes my $kbf = 1024; # Divisionsfaktor für Kilobytes my $bound = "wNWT9spu8GvTg4TJo1iN"; # Boundary for Multipart POST my $excluplddef = ".*@.*"; # vom Upload per default excludierte Objekte (Regex) my $uldcache = $attr{global}{modpath}."/FHEM/FhemUtils/Uploads_SSFile_"; # Filename-Fragment für hochgeladene Files (wird mit Devicename ergänzt) ################################################################ sub Initialize { my ($hash) = @_; $hash->{DefFn} = \&Define; $hash->{UndefFn} = \&Undef; $hash->{DeleteFn} = \&Delete; $hash->{SetFn} = \&Set; $hash->{GetFn} = \&Get; $hash->{AttrFn} = \&Attr; $hash->{DelayedShutdownFn} = \&DelayedShutdown; # Darstellung FHEMWEB # $hash->{FW_summaryFn} = \&FWsummaryFn; # $hash->{FW_addDetailToSummary} = 1 ; # zusaetzlich zu der Device-Summary auch eine Neue mit dem Inhalt von DetailFn angezeigt # $hash->{FW_detailFn} = \&FWdetailFn; $hash->{FW_deviceOverview} = 1; $hash->{AttrList} = "additionalInfo:multiple-strict,real_path,size,owner,time,perm,mount_point_type,type,volume_status ". "disable:1,0 ". "excludeFromUpload:textField-long ". "interval ". "loginRetries:1,2,3,4,5,6,7,8,9,10 ". "noAsyncFillQueue:1,0 ". "showPassInLog:1,0 ". "timeout ". $readingFnAttributes; FHEM::Meta::InitMod( __FILE__, $hash ) if(!$modMetaAbsent); # für Meta.pm (https://forum.fhem.de/index.php/topic,97589.0.html) return; } ################################################################ # define SSFile 192.168.2.10 [5000] [HTTP(S)] # [1] [2] [3] [4] # ################################################################ sub Define { my ($hash, $def) = @_; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; return "Error: Perl module ".$SSFileMM." is missing. Install it on Debian with: sudo apt-get install libjson-perl" if($SSFileMM); my @a = split("[ \t][ \t]*", $def); if(int(@a) < 2) { return "You need to specify more parameters.\n". "Format: define SSFile [Port] [HTTP(S)] [Tasks]"; } shift @a; shift @a; my $addr = ($a[0] && $a[0] ne "Tasks") ? $a[0] : ""; my $port = ($a[1] && $a[1] ne "Tasks") ? $a[1] : 5000; my $prot = ($a[2] && $a[2] ne "Tasks") ? lc($a[2]) : "http"; my $model = "unspecified"; $hash->{SERVERADDR} = $addr; $hash->{SERVERPORT} = $port; $hash->{MODEL} = "Calendar"; $hash->{PROTOCOL} = $prot; $hash->{MODEL} = $model; $hash->{RESEND} = "next planned SendQueue start: immediately by next entry"; $hash->{HELPER}{MODMETAABSENT} = 1 if($modMetaAbsent); # Modul Meta.pm nicht vorhanden CommandAttr(undef, "$name room SSFile"); my $params = { hash => $hash, notes => \%vNotesIntern, useAPI => 1, useSMUtils => 1, useErrCodes => 1 }; use version 0.77; our $VERSION = moduleVersion ($params); # Versionsinformationen setzen getCredentials($hash,1,"credentials",$splitstr); # Credentials lesen $data{$type}{$name}{sendqueue}{index} = 0; # Index der Sendequeue initialisieren initOnBoot($name); # initiale Routinen nach Start ausführen , verzögerter zufälliger Start return; } ################################################################ # Die Undef-Funktion wird aufgerufen wenn ein Gerät mit delete # gelöscht wird oder bei der Abarbeitung des Befehls rereadcfg, # der ebenfalls alle Geräte löscht und danach das # Konfigurationsfile neu einliest. # Funktion: typische Aufräumarbeiten wie das # saubere Schließen von Verbindungen oder das Entfernen von # internen Timern, sofern diese im Modul zum Pollen verwendet # wurden. ################################################################ sub Undef { my $hash = shift; my $arg = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; BlockingKill($hash->{HELPER}{RUNNING_PID}) if($hash->{HELPER}{RUNNING_PID}); delete $data{$type}{$name}; RemoveInternalTimer($name); return; } ####################################################################################################### # Mit der X_DelayedShutdown Funktion kann eine Definition das Stoppen von FHEM verzögern um asynchron # hinter sich aufzuräumen. # Je nach Rückgabewert $delay_needed wird der Stopp von FHEM verzögert (0|1). # Sobald alle nötigen Maßnahmen erledigt sind, muss der Abschluss mit CancelDelayedShutdown($name) an # FHEM zurückgemeldet werden. ####################################################################################################### sub DelayedShutdown { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; if($data{$type}{$name}{uploaded}) { # Cache File für Uploads schreiben my @upl; my $json = encode_json ($data{$type}{$name}{uploaded}); push @upl, $json; my $file = $uldcache.$name; my $error = FileWrite($file, @upl); if ($error) { Log3 ($name, 2, qq{$name - ERROR writing cache file "$file": $error}); } } if($hash->{HELPER}{SID}) { logout($hash, $data{$type}{$name}{fileapi}, $splitstr); # Session alter User beenden falls vorhanden return 1; } return 0; } ################################################################# # Wenn ein Gerät in FHEM gelöscht wird, wird zuerst die Funktion # X_Undef aufgerufen um offene Verbindungen zu schließen, # anschließend wird die Funktion X_Delete aufgerufen. # Funktion: Aufräumen von dauerhaften Daten, welche durch das # Modul evtl. für dieses Gerät spezifisch erstellt worden sind. # Es geht hier also eher darum, alle Spuren sowohl im laufenden # FHEM-Prozess, als auch dauerhafte Daten bspw. im physikalischen # Gerät zu löschen die mit dieser Gerätedefinition zu tun haben. ################################################################# sub Delete { my $hash = shift; my $arg = shift; my $name = $hash->{NAME}; my $index = $hash->{TYPE}."_".$hash->{NAME}."_credentials"; setKeyValue($index, undef); # gespeicherte Credentials löschen my $file = $uldcache.$name; my $error = FileDelete($file); # Cache File für Uploads löschen if ($error) { Log3 ($name, 2, qq{$name - ERROR deleting cache file "$file": $error}); } return; } ################################################################ sub Attr { my $cmd = shift; my $name = shift; my $aName = shift; my $aVal = shift; my $hash = $defs{$name}; my $model = $hash->{MODEL}; my ($do,$val); # $cmd can be "del" or "set" # $name is device name # aName and aVal are Attribute name and value if ($aName eq "disable") { if($cmd eq "set") { $do = $aVal ? 1 : 0; } $do = 0 if($cmd eq "del"); $val = ($do ? "disabled" : "initialized"); if ($do) { delReadings ($name, 0); RemoveInternalTimer($hash); } else { InternalTimer(gettimeofday()+2, "FHEM::SSFile::initOnBoot", $hash, 0) if($init_done); } readingsBeginUpdate($hash); readingsBulkUpdate ($hash, "state", $val); readingsEndUpdate ($hash, 1); } if ($cmd eq "set") { if ($aName =~ m/interval/x && $aVal !~ /^[0-9]+$/x) { return qq{The value of $aName is not valid. Use only integers 0-9 !}; } if($aName =~ m/interval/x) { RemoveInternalTimer($name,"FHEM::SSFile::periodicCall"); InternalTimer (gettimeofday()+1.0, "FHEM::SSFile::periodicCall", $name, 0); } } return; } ############################################################################################# # Setter ############################################################################################# sub Set { my ($hash, @a) = @_; return "\"set X\" needs at least an argument" if ( @a < 2 ); my $name = shift @a; my $opt = shift @a; my $arg = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @a; ## no critic 'Map blocks' my $prop = shift @a; my $model = $hash->{MODEL}; my $type = $hash->{TYPE}; my ($success,$setlist); return if(IsDisabled($name)); my $idxlist = join(",", sort{$a<=>$b} keys %{$data{$type}{$name}{sendqueue}{entries}}); $setlist = "Unknown argument $opt, choose one of "; if(!$hash->{CREDENTIALS}) { # initiale setlist für neue Devices $setlist .= "credentials "; } if($hash->{CREDENTIALS}) { $setlist .= "credentials ". "deleteUploadsDone:noArg ". "deleteRemoteObject:textField-long ". "Download:textField-long ". "Upload:textField-long ". "eraseReadings:noArg ". "listQueue:noArg ". "listUploadsDone:noArg ". "logout:noArg ". "prepareDownload:textField-long ". "prepareUpload:textField-long ". "startQueue:noArg ". ($idxlist ? "purgeQueue:-all-,-permError-,$idxlist " : "purgeQueue:-all-,-permError- ") ; } my $params = { hash => $hash, name => $name, opt => $opt, arg => $arg, prop => $prop }; if($hset{$opt} && defined &{$hset{$opt}{fn}}) { my $ret = q{}; if (!$hash->{CREDENTIALS} && $hset{$opt}{needcred}) { return qq{Credentials of $name are not set. Make sure they are set with "set $name credentials "}; } $ret = &{$hset{$opt}{fn}} ($params); return $ret; } return "$setlist"; } ###################################################################################### # Setter credentials ###################################################################################### sub _setcredentials { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; return qq{The command "$opt" needs an argument.} if (!$arg); my ($a,$h) = parseParams($arg); my $user = $a->[0]; my $pw = $a->[1] // q{}; if($hash->{HELPER}{SID}) { my $type = $hash->{TYPE}; logout($hash, $data{$type}{$name}{fileapi}, $splitstr); # Session alter User beenden falls vorhanden } my ($success) = setCredentials($hash, "credentials", $user, $pw, $splitstr); if($success) { return "credentials saved successfully"; } else { return "Error while saving credentials - see logfile for details"; } return; } ###################################################################################### # Setter Download, prepareDownload ###################################################################################### sub _setDownload { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; if(!$arg) { return qq{The command "$opt" needs an argument !} } # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter> my ($s,$d) = split "dest=", $arg; $d = smUrlEncode ($d); $arg = $s." dest=".$d if($d); my ($a,$h) = parseParams ($arg); my $fp = $a->[0]; if(!$fp) { return qq{No source file or directory specified for download !} } $fp = smUrlEncode ($fp); delReadings ($name, 0); my @dld = split ",", $fp; for my $dl (@dld) { $dl =~ s/"//xg; my $file = (split "\/", $dl)[-1]; my $dest = $h->{dest}; $dl = qq{"$dl"}; if(!$dest) { $dest = $attr{global}{modpath}."/".$file; } if($dest =~ /\/$/x) { $dest .= $file; } my $params = { name => $name, opmode => "download", api => "DOWNLOAD", method => "download", params => "&path=$dl", reqtype => "GET", header => "Accept: application/json", dest => $dest }; addSendqueue ($params); } if($opt ne "prepareDownload") { getApiSites ($name); # Queue starten } else { readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", "none" ); readingsBulkUpdateIfChanged ($hash, "Error", "none" ); readingsBulkUpdate ($hash, "QueueAddDownload", $a->[0]); readingsBulkUpdate ($hash, "state", "done" ); readingsEndUpdate ($hash, 1); } return; } ###################################################################################### # Setter Upload, prepareUpload ###################################################################################### sub _setUpload { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; if(!$arg) { return qq{The command "$opt" needs an argument !} } # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter> my ($a,$h) = parseParams ($arg); my $fp = $a->[0]; if(!$fp) { return qq{No source file or directory specified for upload !} } my $remDir = $h->{dest}; if(!$remDir) { return qq{The command "$opt" needs a destination for upload like "dest=/home/upload" !} } $remDir =~ s/\/$//x; my @t = localtime; $remDir = ResolveDateWildcards ($remDir, @t); # POSIX Wildcards für Verzeichnis auflösen my $ow = $h->{ow} // "true"; # Überschreiben Steuerbit my $cdir = $h->{cdir} // "true"; # create Directory Steuerbit my $mode = $h->{mode} // "full"; # Uploadverfahren (full, inc, new:days) my $struc = $h->{struc} // "true"; # true: Übertragung Struktur erhaltend, false: alles landet im angegebenen Dest-Verzeichnis ohne Berücksichtigung des Quellverezchnisses my @uld = split ",", $fp; my @all; for my $obj (@uld) { # nicht existierende objekte aussondern my @globes = bsd_glob ("$obj"); # Wildcards auflösen (https://perldoc.perl.org/functions/glob) push (@all, @globes); } my @afiles; for my $file (@all) { if (-e $file) { push @afiles, $file; next; } Log3 ($name, 3, qq{$name - The object "$file" doesn't exist or can't be dissolved, ignore it for upload}); next; } readingsSingleUpdate($hash, "state", "Wait for filling upload queue", 1); $paref->{allref} = \@afiles; $paref->{remDir} = $remDir; $paref->{ow} = $ow; $paref->{cdir} = $cdir; $paref->{struc} = $struc; $paref->{mode} = $mode; my $found = exploreFiles ($paref); $paref->{found} = $found; delReadings ($name, 0); delete $paref->{allref}; if(AttrVal($name, "noAsyncFillQueue", 0)) { # kein BlockingCall verwenden __fillUploadQueue ($paref); } else { my $timeout = 1800; $hash->{HELPER}{RUNNING_PID} = BlockingCall("FHEM::SSFile::__fillUploadQueue", $paref, "FHEM::SSFile::__fillUploadQueueFinish", $timeout, "FHEM::SSFile::blockingTimeout", $hash); $hash->{HELPER}{RUNNING_PID}{loglevel} = 5 if($hash->{HELPER}{RUNNING_PID}); # Forum #77057 } return; } ###################################################################################### # extrahierte Filenamen für Upload in Queue eintragen ###################################################################################### sub __fillUploadQueue { my $paref = shift; my $name = $paref->{name}; my $remDir = $paref->{remDir}; my $opt = $paref->{opt}; my $ow = $paref->{ow}; my $cdir = $paref->{cdir}; my $struc = $paref->{struc}; my $found = $paref->{found}; my $hash = $defs{$name}; # Log3 ($name, 3, "$name - all explored files for upload:\n".Dumper $found); for my $sn (keys %{$found}) { my $fname = (split "\/", $found->{$sn}{lfile})[-1]; my $enc = guess_encoding($fname, qw/utf8/); # check ob Name UTF8 kodiert, Forum: https://forum.fhem.de/index.php/topic,115371.msg1158531.html#msg1158531 if(!ref $enc ) { $fname = encode ("utf8", $fname); } my $mtime = $found->{$sn}{mtime} * 1000; # Angabe in Millisekunden my $crtime = $found->{$sn}{crtime} * 1000; # Angabe in Millisekunden my $dir = $remDir.$found->{$sn}{ldir}; # zusammengesetztes Zielverzeichnis (Struktur erhaltend - default) if($struc eq "false") { # Ziel nicht Struktur erhaltend (alle Files landen im Zielverzeichnis ohne Unterverzeichnisse) $dir = $remDir; } my $dat; $dat .= addBodyPart (qq{content-disposition: form-data; name="path"}, $dir, "first"); $dat .= addBodyPart (qq{content-disposition: form-data; name="create_parents"}, $cdir ); $dat .= addBodyPart (qq{content-disposition: form-data; name="overwrite"}, $ow ); $dat .= addBodyPart (qq{content-disposition: form-data; name="mtime"}, $mtime ); $dat .= addBodyPart (qq{content-disposition: form-data; name="crtime"}, $crtime ); $dat .= addBodyPart (qq{content-disposition: form-data; name="file"; filename="$fname"\r\nContent-Type: application/octet-stream}, "", "last" ); my $params = { name => $name, opmode => "upload", api => "UPLOAD", method => "upload", reqtype => "POST", header => "Content-Type: multipart/form-data, boundary=$bound", lclFile => $found->{$sn}{lfile}, postdata => $dat, remFile => $dir."/".$fname }; if(AttrVal($name, "noAsyncFillQueue", 0)) { addSendqueue ($params); } else { my $json = encode_json ($params); BlockingInformParent("FHEM::SSFile::addQueueFromBlocking", [$json], 1); } } if(AttrVal($name, "noAsyncFillQueue", 0)) { return __fillUploadQueueFinish ("$name|$opt"); } return ("$name|$opt"); } #################################################################################################### # Finishing Upload Queue #################################################################################################### sub __fillUploadQueueFinish { my $string = shift; my ($name, $opt) = split "\\|", $string; my $hash = $defs{$name}; readingsSingleUpdate($hash, "state", "Upload queue fill finished", 1); my $ql = ReadingsVal($name, "QueueLength", 0); # zusätzlichen Event wenn Queue Aufbau fertig CommandTrigger(undef, "$name QueueLength: $ql"); if($opt ne "prepareUpload") { getApiSites ($name); # Queue starten } else { readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", "none"); readingsBulkUpdateIfChanged ($hash, "Error", "none"); readingsBulkUpdate ($hash, "state", "done"); readingsEndUpdate ($hash, 1); } return; } ################################################################### # SendQueue aus BlockingCall heraus füllen ################################################################### sub addQueueFromBlocking { my $json = shift; my $params = decode_json ($json); addSendqueue ($params); return 1; } ###################################################################################### # Setter startQueue ###################################################################################### sub _setstartQueue { my $paref = shift; my $name = $paref->{name}; my $ret = getApiSites($name); if($ret) { return $ret; } else { return "The SendQueue has been restarted."; } return; } ###################################################################################### # Setter eraseReadings ###################################################################################### sub _seteraseReadings { my $paref = shift; my $name = $paref->{name}; delReadings($name, 0); # Readings löschen return; } ###################################################################################### # Setter logout ###################################################################################### sub _setlogout { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $type = $hash->{TYPE}; logout($hash, $data{$type}{$name}{fileapi}, $splitstr); return; } ###################################################################################### # Setter listUploadsDone ###################################################################################### sub _setlistUploadsDone { my $paref = shift; my $hash = $paref->{hash}; my $ret = listUploadsDone ($hash); return $ret; } ###################################################################################### # Setter deleteUploadsDone ###################################################################################### sub _setdeleteUploadsDone { my $paref = shift; my $name = $paref->{name}; my $hash = $paref->{hash}; my $type = $hash->{TYPE}; delete $data{$type}{$name}{uploaded}; return; } ###################################################################################### # Setter _setdeleteRemoteObject ###################################################################################### sub _setdeleteRemoteObject { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; if(!$arg) { return qq{The command "$opt" needs an argument !} } delReadings ($name, 0); $arg = smUrlEncode ($arg); my ($a,$h) = parseParams ($arg); my $fp = $a->[0]; if(!$fp) { return qq{No source file or directory specified for upload !} } my $recursive = $h->{recursive} // "true"; # true: Dateien rekursiv löschen innerhalb eines Ordners, false: Nur Datei/Ordner der ersten Ebene löschen my @del = split ",", $fp; for my $dl (@del) { $dl =~ s/"//xg; $dl = qq{"$dl"}; my $params = { name => $name, opmode => "deleteRemoteObj", api => "DELETE", method => "delete", params => "&recursive=$recursive&path=$dl", reqtype => "GET", header => "Accept: application/json", timeout => 600, }; addSendqueue ($params); } getApiSites ($name); # Queue starten return; } ###################################################################################### # Getter ###################################################################################### sub Get { my ($hash, @a) = @_; return "\"get X\" needs at least an argument" if ( @a < 2 ); my $name = shift @a; my $opt = shift @a; my $arg = join " ", map { my $p = $_; $p =~ s/\s//xg; $p; } @a; ## no critic 'Map blocks' my $getlist; if(!$hash->{CREDENTIALS}) { return; } else { $getlist = "Unknown argument $opt, choose one of ". "apiInfo:noArg ". "backgroundTaskList:noArg ". "fileStationInfo:noArg ". "remoteFolderList ". "remoteFileInfo:textField-long ". "storedCredentials:noArg ". "versionNotes " ; } return if(IsDisabled($name)); my $params = { hash => $hash, name => $name, opt => $opt, arg => $arg }; if($hget{$opt} && defined &{$hget{$opt}{fn}}) { my $ret = q{}; if (!$hash->{CREDENTIALS} && $hget{$opt}{needcred}) { return qq{Credentials of $name are not set. Make sure they are set with "set $name credentials "}; } $ret = &{$hget{$opt}{fn}} ($params); return $ret; } return $getlist; } ###################################################################################### # Getter storedCredentials ###################################################################################### sub _getstoredCredentials { my $paref = shift; my $hash = $paref->{hash}; my $out = showStoredCredentials ($hash, 1, $splitstr); return $out; } ###################################################################################### # Getter apiInfo # Informationen der verwendeten API abrufen und anzeigen ###################################################################################### sub _getapiInfo { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; delClHash ($name); getClHash ($hash,1); # übergebenen CL-Hash (FHEMWEB) in Helper eintragen # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter my $params = { name => $name, opmode => "apiInfo", api => "", method => "", params => "", reqtype => "GET", header => "Accept: application/json" }; addSendqueue ($params); getApiSites ($name); return; } ###################################################################################### # Getter fileStationInfo # Informationen der File Station abrufen ###################################################################################### sub _getfilestationInfo { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter> my $params = { name => $name, opmode => "fileStationInfo", api => "FSINFO", method => "get", # Methode get statt getinfo -> Fehler in Docu ! params => "", reqtype => "GET", header => "Accept: application/json" }; addSendqueue ($params); getApiSites ($name); return; } ###################################################################################### # Getter backgroundTaskList # Informationen über Operationen der File Station die im Hintergrund laufen ###################################################################################### sub _getbackgroundTaskList { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter> my $params = { name => $name, opmode => "backgroundTask", api => "BGTASK", method => "list", params => "", reqtype => "GET", header => "Accept: application/json" }; addSendqueue ($params); getApiSites ($name); return; } ###################################################################################### # Getter remoteFolderList # Alle freigegebenen Ordner auflisten, Dateien in einem freigegebenen Ordner # aufzählen und detaillierte Dateiinformationen erhalten ###################################################################################### sub _getRemoteFolderList { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $arg = $paref->{arg}; my $params; my $adds = AttrVal($name, "additionalInfo", ""); # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter> if ($arg) { $arg = smUrlEncode ($arg); my ($a,$h) = parseParams ($arg); my $fp = $a->[0]; my $mo = q{}; while (my ($k,$v) = each %$h) { # Zusatzoptionen z.B. filetype=file $mo .= "&".$k."=".$v; } $params = { name => $name, opmode => "remoteFolderList", api => "LIST", method => "list", params => "&additional=$adds&folder_path=$fp".$mo, reqtype => "GET" }; } else { $params = { name => $name, opmode => "shareList", api => "LIST", method => "list_share", params => "&additional=$adds", reqtype => "GET", header => "Accept: application/json", }; } addSendqueue ($params); getApiSites ($name); return; } ###################################################################################### # Getter filefolderInfo # Informationen über die Datei(en) erhalten ###################################################################################### sub _getremoteFileInfo { my $paref = shift; my $hash = $paref->{hash}; my $name = $paref->{name}; my $opt = $paref->{opt}; my $arg = $paref->{arg}; if(!$arg) { return qq{The command "$opt" needs an argument !} } my $params; my $adds = AttrVal($name, "additionalInfo", ""); # Schema: name => Name, # opmode => operation mode, # api => API (siehe $data{$type}{$name}{fileapi}), # method => auszuführende API-Methode, # params => "spezifische API-Parameter> $arg = smUrlEncode ($arg); my ($a,$h) = parseParams ($arg); my $fp = $a->[0]; my $mo = q{}; while (my ($k,$v) = each %$h) { # Zusatzoptionen z.B. filetype=file $mo .= "&".$k."=".$v; } $params = { name => $name, opmode => "remoteFileInfo", api => "LIST", method => "getinfo", params => "&additional=$adds&path=$fp".$mo, reqtype => "GET", header => "Accept: application/json" }; addSendqueue ($params); getApiSites ($name); return; } ###################################################################################### # Getter versionNotes ###################################################################################### sub _getversionNotes { my $paref = shift; $paref->{hintextde} = \%vHintsExt_de; $paref->{hintexten} = \%vHintsExt_en; $paref->{notesext} = \%vNotesExtern; my $ret = showModuleInfo ($paref); return $ret; } ###################################################################################### # initiale Startroutinen nach Restart FHEM ###################################################################################### sub initOnBoot { my $name = shift; my $hash = $defs{$name}; my $type = $hash->{TYPE}; my $ret; RemoveInternalTimer($name, "FHEM::SSFile::initOnBoot"); if ($init_done) { my $file = $uldcache.$name; my ($error, @content) = FileRead ($file); # Cache File der Uploads lesen wenn vorhanden if(!$error) { my $json = join "", @content; my $success = evaljson ($hash, $json); # V0.7.7 07.01.2021 if($success) { $data{$type}{$name}{uploaded} = decode_json ($json); } else { Log3($name, 2, qq{$name - WARNING - the content of file "$file" is not readable and may be corrupt}); } } readingsBeginUpdate($hash); readingsBulkUpdate ($hash, "Errorcode" , "none"); readingsBulkUpdate ($hash, "Error" , "none"); readingsBulkUpdate ($hash, "QueueLength", 0); # Länge Sendqueue initialisieren readingsBulkUpdate ($hash, "nextUpdate" , "undefined"); # Abrufmode initialisieren readingsBulkUpdate ($hash, "state" , "Initialized"); # Init state readingsEndUpdate ($hash,1); } else { InternalTimer(gettimeofday()+3, "FHEM::SSFile::initOnBoot", $name, 0); } return; } ############################################################################################# # regelmäßiger Intervallabruf ############################################################################################# sub periodicCall { my $name = shift; my $hash = $defs{$name}; RemoveInternalTimer($name,"FHEM::SSFile::periodicCall"); my $interval = AttrVal($name, "interval", 0); my $new; if(!$interval) { $hash->{MODE} = "Manual"; readingsSingleUpdate($hash, "nextUpdate", "manual", 1); return; } else { $new = gettimeofday()+$interval; readingsBeginUpdate ($hash); readingsBulkUpdate ($hash, "nextUpdate", "Automatic - next start Queue time: ".FmtTime($new)); # Abrufmode initial auf "Manual" setzen readingsEndUpdate ($hash,1); $hash->{MODE} = "Automatic"; } if(!IsDisabled($name) && ReadingsVal($name, "state", "running") ne "running") { getApiSites($name); # Queue starten } InternalTimer($new, "FHEM::SSFile::periodicCall", $name, 0); return; } #################################################################################### # Einstiegsfunktion Queue Abarbeitung #################################################################################### sub getApiSites { my $name = shift; my $hash = $defs{$name}; my $addr = $hash->{SERVERADDR}; my $port = $hash->{SERVERPORT}; my $prot = $hash->{PROTOCOL}; my $type = $hash->{TYPE}; my ($url,$idxset,$ret); $hash->{HELPER}{LOGINRETRIES} = 0; if(!keys %{$data{$type}{$name}{sendqueue}{entries}}) { $ret = "Sendqueue is empty. Nothing to do ..."; Log3($name, 4, "$name - $ret"); return $ret; } if($hash->{OPMODE}) { # Überholer vermeiden wenn eine Operation läuft (V. 0.7.5" => "07.12.2020) Log3($name, 4, qq{$name - Operation "$hash->{OPMODE} (idx: $hash->{OPIDX})" is still running. Next operation start postponed}); return; } # den nächsten Eintrag aus "SendQueue" selektieren und ausführen wenn nicht forbidSend gesetzt ist for my $idx (sort{$a<=>$b} keys %{$data{$type}{$name}{sendqueue}{entries}}) { if (!$data{$type}{$name}{sendqueue}{entries}{$idx}{forbidSend}) { $hash->{OPIDX} = $idx; $hash->{OPMODE} = $data{$type}{$name}{sendqueue}{entries}{$idx}{opmode}; $idxset = 1; last; } } if(!$idxset) { $ret = qq{Only entries with "forbidSend" are in Sendqueue. Escaping ...}; Log3($name, 4, "$name - $ret"); return $ret; } Log3($name, 4, "$name - ####################################################"); Log3($name, 4, "$name - ### start Synology File operation $hash->{OPMODE} "); Log3($name, 4, "$name - ####################################################"); readingsBeginUpdate ($hash); readingsBulkUpdate ($hash, "state", "running"); readingsEndUpdate ($hash, 1); if ($hash->{OPMODE} eq "apiInfo") { $data{$type}{$name}{fileapi}{PARSET} = 0; # erzwinge Abruf API } if ($data{$type}{$name}{fileapi}{PARSET}) { # API-Hashwerte sind bereits gesetzt -> Abruf überspringen Log3($name, 4, "$name - API hash values already set - ignore get apisites"); return checkSID($name); } my $timeout = AttrVal($name, "timeout", 20); Log3($name, 5, "$name - HTTP-Call will be done with timeout: $timeout s"); # API initialisieren und abrufen #################################### $data{$type}{$name}{fileapi} = apistatic ("file"); # API Template im HELPER instanziieren Log3 ($name, 4, "$name - API imported:\n".Dumper $data{$type}{$name}{fileapi}); my @ak; for my $key (keys %{$data{$type}{$name}{fileapi}}) { next if($key =~ /^PARSET$/x); push @ak, $data{$type}{$name}{fileapi}{$key}{NAME}; } my $apis = join ",", @ak; my $fileapi = $data{$type}{$name}{fileapi}; $url = "$prot://$addr:$port/webapi/$data{$type}{$name}{fileapi}{INFO}{PATH}?". "api=$data{$type}{$name}{fileapi}{INFO}{NAME}". "&method=Query". "&version=$data{$type}{$name}{fileapi}{INFO}{VER}". "&query=$apis"; Log3($name, 4, "$name - Call-Out: $url"); my $param = { url => $url, timeout => $timeout, hash => $hash, method => "GET", header => "Accept: application/json", callback => \&FHEM::SSFile::getApiSites_parse }; HttpUtils_NonblockingGet ($param); return; } #################################################################################### # Auswertung Abruf apisites #################################################################################### sub getApiSites_parse { ## no critic 'complexity' my ($param, $err, $myjson) = @_; my $hash = $param->{hash}; my $name = $hash->{NAME}; my $opmode = $hash->{OPMODE}; my $type = $hash->{TYPE}; my ($error,$errorcode,$success); if ($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist Log3($name, 2, "$name - ERROR message: $err"); setReadingErrorState ($hash, $err); checkSendRetry ($name, 1, $queueStartFn); return; } elsif ($myjson ne "") { ($success) = evaljson($hash,$myjson); if (!$success) { Log3 ($name, 4, "$name - Data returned: ".$myjson); checkSendRetry ($name, 1, $queueStartFn); return; } my $jdata = decode_json($myjson); Log3($name, 5, "$name - JSON returned: ". Dumper $jdata); $success = $jdata->{'success'}; if ($success) { my $completed = completeAPI ($jdata, $data{$type}{$name}{fileapi}); # übergibt Referenz zum instanziierten API-Hash if(!$completed) { $errorcode = "9001"; $error = expErrors($hash,$errorcode); # Fehlertext zum Errorcode ermitteln setReadingErrorState ($hash, $error, $errorcode); Log3($name, 2, "$name - ERROR - $error"); checkSendRetry ($name, 1, $queueStartFn); return; } ### Downgrades für nicht kompatible API-Versionen. Hier nur nutzen wenn API zentral downgraded werden soll Log3($name, 4, "$name - ------- Begin of adaption section -------"); my $adavs = "a01"; # adaptierte Version if($adavs) { for my $av (sort keys %{$hvada{$adavs}}) { $data{$type}{$name}{fileapi}{$av}{VER} = $hvada{$adavs}{$av}; $data{$type}{$name}{fileapi}{$av}{MOD} = "yes"; Log3($name, 4, "$name - Version of $data{$type}{$name}{fileapi}{$av}{NAME} adapted to: $data{$type}{$name}{fileapi}{$av}{VER}"); } } Log3($name, 4, "$name - ------- End of adaption section -------"); setReadingErrorNone($hash, 1); Log3 ($name, 4, "$name - API completed:\n".Dumper $data{$type}{$name}{fileapi}); if ($opmode eq "apiInfo") { # API Infos in Popup anzeigen showAPIinfo ($hash, $data{$type}{$name}{fileapi}); # übergibt Referenz zum instanziierten API-Hash) readingsSingleUpdate ($hash, "state", "done", 1); checkSendRetry ($name, 0, $queueStartFn); return; } } else { $errorcode = "806"; $error = expErrors($hash,$errorcode); # Fehlertext zum Errorcode ermitteln readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); readingsBulkUpdateIfChanged ($hash, "Error", $error); readingsBulkUpdate ($hash, "state", "Error"); readingsEndUpdate ($hash, 1); Log3($name, 2, "$name - ERROR - the API-Query couldn't be executed successfully"); checkSendRetry ($name, 1, $queueStartFn); return; } } return checkSID($name); } ############################################################################################# # Ausführung Operation ############################################################################################# sub execOp { my $name = shift; my $hash = $defs{$name}; my $prot = $hash->{PROTOCOL}; my $addr = $hash->{SERVERADDR}; my $port = $hash->{SERVERPORT}; my $sid = $hash->{HELPER}{SID}; my $type = $hash->{TYPE}; my $idx = $hash->{OPIDX}; my $opmode = $hash->{OPMODE}; my $method = $data{$type}{$name}{sendqueue}{entries}{$idx}{method}; my $api = $data{$type}{$name}{sendqueue}{entries}{$idx}{api}; my $params = $data{$type}{$name}{sendqueue}{entries}{$idx}{params}; my $reqtype = $data{$type}{$name}{sendqueue}{entries}{$idx}{reqtype}; my $header = $data{$type}{$name}{sendqueue}{entries}{$idx}{header}; my $postdata = $data{$type}{$name}{sendqueue}{entries}{$idx}{postdata}; my $toutdef = $data{$type}{$name}{sendqueue}{entries}{$idx}{timeout} // 20; my $fileapi = $data{$type}{$name}{fileapi}; my ($url,$param,$error,$errorcode,$content); my $timeout = AttrVal($name, "timeout", $toutdef); Log3($name, 4, "$name - start SendQueue entry index \"$idx\" ($hash->{OPMODE}) for operation."); Log3($name, 5, "$name - HTTP-Call will be done with timeout: $timeout s"); $param = { timeout => $timeout, hash => $hash, method => $reqtype, header => $header, callback => \&FHEM::SSFile::execOp_parse }; if($reqtype eq "GET") { $url = "$prot://$addr:$port/webapi/".$fileapi->{$api}{PATH}."?api=".$fileapi->{$api}{NAME}."&version=".$fileapi->{$api}{VER}."&method=$method".$params."&_sid=$sid"; logUrl ($name, $url); $param->{url} = $url; } if($reqtype eq "POST") { $url = "$prot://$addr:$port/webapi/".$fileapi->{$api}{PATH}."?api=".$fileapi->{$api}{NAME}."&version=".$fileapi->{$api}{VER}."&method=$method&_sid=$sid"; logUrl ($name, $url); my $lclFile = $data{$type}{$name}{sendqueue}{entries}{$idx}{lclFile}; Log3($name, 5, "$name - POST data (string will be replaced with content of $lclFile):\n$postdata"); ($errorcode, $content) = slurpFile ($name, $lclFile); if($errorcode) { $error = expErrors ($hash, $errorcode ); setReadingErrorState ($hash, $error, $errorcode ); checkSendRetry ($name, 1, $queueStartFn); undef $content; return; } $postdata =~ s//$content/xs; $param->{url} = $url; $param->{data} = $postdata; } HttpUtils_NonblockingGet ($param); undef $content; undef $postdata; return; } ############################################################################################# # Callback from execOp ############################################################################################# sub execOp_parse { my $param = shift; my $err = shift; my $myjson = shift; my $hash = $param->{hash}; my $head = $param->{httpheader}; my $name = $hash->{NAME}; my $prot = $hash->{PROTOCOL}; my $addr = $hash->{SERVERADDR}; my $port = $hash->{SERVERPORT}; my $opmode = $hash->{OPMODE}; my $type = $hash->{TYPE}; my ($jdata,$success,$error,$errorcode,$cherror); if ($err ne "") { # wenn ein Fehler bei der HTTP Abfrage aufgetreten ist Log3($name, 2, "$name - ERROR message: $err"); $errorcode = "none"; $errorcode = "800" if($err =~ /:\smalformed\sor\sunsupported\sURL$/xs); readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Error", $err); readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode); readingsBulkUpdate ($hash, "state", "Error"); readingsEndUpdate ($hash,1); checkSendRetry ($name, 1, $queueStartFn); return; } elsif ($myjson ne "") { # wenn die Abfrage erfolgreich war ($data enthält die Ergebnisdaten des HTTP Aufrufes) if($opmode ne "download") { $success = evaljson ($hash, $myjson); if (!$success) { Log3 ($name, 4, "$name - Data returned: ".$myjson); checkSendRetry ($name, 1, $queueStartFn); return; } eval { $jdata = decode_json($myjson); }; ## no critic 'eval not tested' #Forum: https://forum.fhem.de/index.php/topic,115371.msg1158531.html#msg1158531 Log3($name, 5, "$name - JSON returned: ". Dumper $jdata); $success = $jdata->{'success'}; } else { # Opmode download bringt File kein JSON $success = 1; $jdata = $myjson; } if ($success) { my %ra; # Hash für Ergebnisreadings my $ret = q{}; $error = "none"; my $state = "done"; my $params = { hash => $hash, param => $param, jdata => $jdata, href => \%ra, }; if($hmodep{$opmode} && defined &{$hmodep{$opmode}{fn}}) { $ret = &{$hmodep{$opmode}{fn}} ($params) // q{}; undef $params; } else { Log3($name, 1, qq{$name - ERROR - no operation parse function found for "$opmode"}); checkSendRetry ($name, 0, $queueStartFn); return; } _createReadings ($name, $ret, \%ra); } else { # die API-Operation war fehlerhaft Log3 ($name, 5, "$name - Header returned:\n".$head); $errorcode = $jdata->{error}->{code}; $cherror = $jdata->{error}->{errors}; $error = expErrors($hash,$errorcode) // q{}; # Fehlertext zum Errorcode ermitteln if ($error =~ /not found/) { $error .= " New error: ".($cherror // "' '"); } setReadingErrorState ($hash, $error, $errorcode); Log3($name, 2, "$name - ERROR - Operation $opmode was not successful. Errorcode: $errorcode - $error"); checkSendRetry ($name, 1, $queueStartFn); } undef $myjson; undef $jdata; } return; } ############################################################################################# # erstellt Readings aus einem übergebenen Reading Hashref ($href) ############################################################################################# sub _createReadings { my $name = shift; my $ret = shift; my $href = shift; my $hash = $defs{$name}; my $type = $hash->{TYPE}; my $error = "none"; my $errorcode = "none"; my $state = "done"; if($ret) { $errorcode = $ret; $error = expErrors($hash,$errorcode); # Fehlertext zum Errorcode ermitteln $state = "Error"; } my $opmode = $hash->{OPMODE}; my $evt = $hmodep{$opmode}{doevt}; readingsBeginUpdate ($hash); $data{$type}{$name}{lastUpdate} = FmtDateTime($hash->{".updateTime"}); # letzte Updatezeit speichern while (my ($k, $v) = each %$href) { readingsBulkUpdate ($hash, $k, $v); } readingsEndUpdate ($hash, $evt); readingsBeginUpdate ($hash); readingsBulkUpdateIfChanged ($hash, "Errorcode", $errorcode ); readingsBulkUpdateIfChanged ($hash, "Error", $error ); readingsBulkUpdate ($hash, "lastUpdate", FmtDateTime(time)); readingsBulkUpdate ($hash, "state", $state ); readingsEndUpdate ($hash, 1); delReadings($name,1) if($data{$type}{$name}{lastUpdate}); # Readings löschen wenn Timestamp nicht "lastUpdate" checkSendRetry ($name, 0, $queueStartFn); return; } ############################################################################################# # File Station Info parsen ############################################################################################# sub _parsefilestationInfo { my $paref = shift; my $hash = $paref->{hash}; my $jdata = $paref->{jdata}; my $href = $paref->{href}; my $name = $hash->{NAME}; my $is_manager = jboolmap ($jdata->{data}{is_manager}); my $hostname = $jdata->{data}{hostname}; my $svfs = jboolmap ($jdata->{data}{support_vfs}); my $sfr = jboolmap ($jdata->{data}{support_file_request}); my $sshare = jboolmap ($jdata->{data}{support_sharing}); my $eisom = jboolmap ($jdata->{data}{support_virtual}{enable_iso_mount}); my $remom = jboolmap ($jdata->{data}{support_virtual}{enable_remote_mount}); my $uid = $jdata->{data}{uid}; my $cp = $jdata->{data}{system_codepage}; my $sproto = join ",", @{$jdata->{data}{support_virtual_protocol}}; $href->{IsUserManager} = $is_manager; $href->{Hostname} = $hostname; $href->{Support_vfs} = $svfs; $href->{Support_filerequest} = $sfr; $href->{Support_sharing} = $sshare; $href->{Support_protocols} = $sproto; $href->{UID} = $uid; $href->{SystemCodepage} = $cp; $href->{Enabled_iso_mount} = $eisom; $href->{Enabled_remote_mount} = $remom; return; } ############################################################################################# # File Station laufende Background Tasks parsen ############################################################################################# sub _parsebackgroundTaskList { my $paref = shift; my $hash = $paref->{hash}; my $jdata = $paref->{jdata}; my $href = $paref->{href}; my $name = $hash->{NAME}; my $total = $jdata->{data}{total}; my $tasks = join ",\n", @{$jdata->{data}{tasks}}; $tasks = $tasks ? $tasks : "no running tasks"; $href->{BackgroundTasksNum} = $total; $href->{BackgroundTasks} = $tasks; return; } ############################################################################################# # File / Folder Info parsen Intro ############################################################################################# sub _parseFiFo { my $paref = shift; my $hash = $paref->{hash}; my $jdata = $paref->{jdata}; my $href = $paref->{href}; # Hash Referenz für Ergebnisreadings my $name = $hash->{NAME}; my $opmode = $hash->{OPMODE}; my $qal; if($opmode eq "shareList") { $qal = "shares"; } elsif ($opmode eq "remoteFolderList") { $qal = "files"; } elsif ($opmode eq "remoteFileInfo") { $qal = "files"; } else { Log3($name, 1, "$name - ERROR - no valid operation mode set for function ".(caller(0))[3]); return; } my $total = $jdata->{data}{total} // $jdata->{data}{$qal} ? scalar(@{$jdata->{data}{$qal}}) : 0; my $len = length $total; my $params = { name => $name, jdata => $jdata, total => $total, len => $len, qal => $qal, href => $href }; my $ec = __createKeyValueHash ($params); return $ec if($ec); return; } ############################################################################################# # File / Folder Info parsen ############################################################################################# sub __createKeyValueHash { my $paref = shift; my $name = $paref->{name}; my $jdata = $paref->{jdata}; my $total = $paref->{total}; my $len = $paref->{len}; my $qal = $paref->{qal}; my $href = $paref->{href}; my $adds = AttrVal($name, "additionalInfo", ""); for (my $i=0; $i<$total; $i++) { if($jdata->{data}{$qal}[$i]{code}) { return ($jdata->{data}{$qal}[$i]{code}); # File not found bei "getinfo" File } $href->{sprintf("%0$len.0f", $i+1)."_IsDir"} = jboolmap ($jdata->{data}{$qal}[$i]{isdir}) if(defined $jdata->{data}{$qal}[$i]{isdir}); $href->{sprintf("%0$len.0f", $i+1)."_Path"} = encode("utf8", $jdata->{data}{$qal}[$i]{path}) if(defined $jdata->{data}{$qal}[$i]{path}); $href->{sprintf("%0$len.0f", $i+1)."_Path_real"} = encode("utf8", $jdata->{data}{$qal}[$i]{additional}{real_path}) if(defined $jdata->{data}{$qal}[$i]{additional}{real_path}); $href->{sprintf("%0$len.0f", $i+1)."_Type"} = $jdata->{data}{$qal}[$i]{additional}{type} if($jdata->{data}{$qal}[$i]{additional}{type}); $href->{sprintf("%0$len.0f", $i+1)."_MBytes_total"} = int $jdata->{data}{$qal}[$i]{additional}{volume_status}{totalspace} / $mbf if(defined $jdata->{data}{$qal}[$i]{additional}{volume_status}{totalspace}); $href->{sprintf("%0$len.0f", $i+1)."_MBytes_free"} = int $jdata->{data}{$qal}[$i]{additional}{volume_status}{freespace} / $mbf if(defined $jdata->{data}{$qal}[$i]{additional}{volume_status}{freespace}); $href->{sprintf("%0$len.0f", $i+1)."_MBytes_Size"} = sprintf("%0.3f", $jdata->{data}{$qal}[$i]{additional}{size} / $mbf) if(defined $jdata->{data}{$qal}[$i]{additional}{size}); $href->{sprintf("%0$len.0f", $i+1)."_Readonly"} = jboolmap ($jdata->{data}{$qal}[$i]{additional}{volume_status}{readonly}) if(defined $jdata->{data}{$qal}[$i]{additional}{volume_status}{readonly}); $href->{sprintf("%0$len.0f", $i+1)."_MountpointType"} = $jdata->{data}{$qal}[$i]{additional}{mount_point_type} if(defined $jdata->{data}{$qal}[$i]{additional}{mount_point_type}); if($jdata->{data}{$qal}[$i]{additional}{perm}{acl}) { $href->{sprintf("%0$len.0f", $i+1)."_Perm_Posix"} = $jdata->{data}{$qal}[$i]{additional}{perm}{posix}; $href->{sprintf("%0$len.0f", $i+1)."_Perm_Share"} = $jdata->{data}{$qal}[$i]{additional}{perm}{share_right}; $href->{sprintf("%0$len.0f", $i+1)."_Is_ACL_Mode"} = jboolmap ($jdata->{data}{$qal}[$i]{additional}{perm}{is_acl_mode}); my @acl; while (my ($k,$v) = each %{$jdata->{data}{$qal}[$i]{additional}{perm}{acl}}) { push @acl, "$k:$v"; } $href->{sprintf("%0$len.0f", $i+1)."_Perm_ACL"} = join ", ", @acl; } if($jdata->{data}{$qal}[$i]{additional}{time}) { $href->{sprintf("%0$len.0f", $i+1)."_Time_modified"} = FmtDateTime ($jdata->{data}{$qal}[$i]{additional}{time}{mtime}); $href->{sprintf("%0$len.0f", $i+1)."_Time_changed"} = FmtDateTime ($jdata->{data}{$qal}[$i]{additional}{time}{ctime}); $href->{sprintf("%0$len.0f", $i+1)."_Time_accessed"} = FmtDateTime ($jdata->{data}{$qal}[$i]{additional}{time}{atime}); $href->{sprintf("%0$len.0f", $i+1)."_Time_created"} = FmtDateTime ($jdata->{data}{$qal}[$i]{additional}{time}{crtime}); } if($jdata->{data}{$qal}[$i]{additional}{owner}) { $href->{sprintf("%0$len.0f", $i+1)."_Owner_GID"} = $jdata->{data}{$qal}[$i]{additional}{owner}{gid}; $href->{sprintf("%0$len.0f", $i+1)."_Owner_User"} = $jdata->{data}{$qal}[$i]{additional}{owner}{user}; $href->{sprintf("%0$len.0f", $i+1)."_Owner_Group"} = $jdata->{data}{$qal}[$i]{additional}{owner}{group}; $href->{sprintf("%0$len.0f", $i+1)."_Owner_UID"} = $jdata->{data}{$qal}[$i]{additional}{owner}{uid}; } if($adds) { $href->{sprintf("%0$len.0f", $i+1)."__----------------------"} = "--------------------------------------------------------------------"; } } $href->{EntriesTotalNum} = $total; return; } ############################################################################################# # File Station File Download parse ############################################################################################# sub _parseDownload { my $paref = shift; my $hash = $paref->{hash}; my $jdata = $paref->{jdata}; my $param = $paref->{param}; my $href = $paref->{href}; my $head = $param->{httpheader}; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $idx = $hash->{OPIDX}; my $obj = urlDecode ((split "=", $data{$type}{$name}{sendqueue}{entries}{$idx}{params})[1]); Log3 ($name, 5, "$name - Header returned:\n".$head); if($head =~ /404\sNot\sFound/xms) { Log3 ($name, 2, qq{$name - ERROR - Object $obj not found for download}); return 9002; # return Errorcode } if($head =~ /400\sBad\sRequest/xms) { Log3 ($name, 2, qq{$name - ERROR - Object $obj - Bad Request}); return 9003; # return Errorcode } my $err; my $sp = q{}; my $dest = urlDecode ($data{$type}{$name}{sendqueue}{entries}{$idx}{dest}); open my $fh, '>', $dest or do { $err = qq{Can't open file "$dest": $!}; Log3($name, 2, "$name - $err"); return 417; # return Errorcode }; if(!$err) { binmode $fh; print $fh $jdata; close $fh; } $href->{LocalFile} = $dest; Log3 ($name, 3, qq{$name - Object $obj downloaded to "$dest"}); return; } ############################################################################################# # File Station File Upload parse ############################################################################################# sub _parseUpload { my $paref = shift; my $hash = $paref->{hash}; my $jdata = $paref->{jdata}; my $href = $paref->{href}; # Hash Referenz für Ergebnisreadings my $param = $paref->{param}; my $head = $param->{httpheader}; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $idx = $hash->{OPIDX}; Log3 ($name, 5, "$name - Header returned:\n".$head); my $skip = jboolmap ($jdata->{data}{blSkip}); my $file = $jdata->{data}{file}; $href->{FileWriteSkipped} = $skip; $href->{PID} = $jdata->{data}{pid}; $href->{Progress} = $jdata->{data}{progress}; $href->{RemoteFile} = encode("utf8", $file); my $lclobj = $data{$type}{$name}{sendqueue}{entries}{$idx}{lclFile}; # lokales File-Objekt des aktuellen Index my $remobj = $data{$type}{$name}{sendqueue}{entries}{$idx}{remFile}; # File-Objekt im Zielverezichnis my $trtxt; if($skip eq "false") { $data{$type}{$name}{uploaded}{"$lclobj"} = { remobj => $remobj, done => 1, ts => time }; # Status und Zeit des Objekt-Upload speichern Log3 ($name, 4, qq{$name - Object "$lclobj" uploaded}); $trtxt = qq{Uploaded: local File "$lclobj" to remote File "$remobj"}; } else { Log3 ($name, 3, qq{$name - Object "$remobj" already exists -> upload skipped}); $trtxt = qq{Upload: skipped upload local File "$lclobj"}; } CommandTrigger(undef, "$name $trtxt"); return; } ############################################################################################# # delete Objekt parse Funktion # $jdata: decodierte received JSON Data # $href: Referenz zum Hash für erstellte Readings ############################################################################################# sub _parsedeleteRemoteObject { my $paref = shift; my $hash = $paref->{hash}; my $jdata = $paref->{jdata}; my $href = $paref->{href}; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; my $idx = $hash->{OPIDX}; my $obj = urlDecode ((split "path=", $data{$type}{$name}{sendqueue}{entries}{$idx}{params})[1]); Log3 ($name, 3, qq{$name - remote Object $obj deleted}); $href->{deletedRemoteFile} = $obj; return; } ############################################################################################# # check SID ############################################################################################# sub checkSID { my $name = shift; my $hash = $defs{$name}; my $type = $hash->{TYPE}; # SID holen bzw. login my $subref = \&execOp; if(!$hash->{HELPER}{SID}) { Log3 ($name, 3, "$name - no session ID found - get new one"); login ($hash, $data{$type}{$name}{fileapi}, $subref, $name, $splitstr); return; } return execOp($name); } ############################################################################################# # zu sendende URL ins Log schreiben ############################################################################################# sub logUrl { my $name = shift; my $url = shift; my $hash = $defs{$name}; my $sid = $hash->{HELPER}{SID}; if(AttrVal($name, "showPassInLog", 0)) { Log3($name, 4, "$name - Call-Out: $url"); } else { $url =~ s/$sid//x; Log3($name, 4, "$name - Call-Out: $url"); } return; } ############################################################################################# # erstelle einen Part für POST multipart/form-data # # $seq: first - den ersten CRLF imBody nicht schreiben wegen HttpUtils Problem: # https://forum.fhem.de/index.php/topic,115156.0.html # last - Ergänzung des letzten Boundary # ############################################################################################# sub addBodyPart { my $cdisp = shift; my $val = shift; my $seq = shift // ""; # Sequence: first, last my $part; $part .= "\r\n" if($seq ne "first"); $part .= "--".$bound; $part .= "\r\n"; $part .= $cdisp; $part .= "\r\n"; $part .= "\r\n"; $part .= $val; $part .= "\r\n--".$bound."-- " if($seq eq "last"); return $part; } ############################################################################################# # File::Find Explore Files & Directories # -M File Operator: Skript-Startzeit minus Datei-Änderungszeit, in Tagen ############################################################################################# sub exploreFiles { my $paref = shift; my $allref = $paref->{allref}; my $name = $paref->{name}; my $mode = $paref->{mode}; # Backup/Upload-Mode my $hash = $paref->{hash}; my $type = $hash->{TYPE}; my $lu = ReadingsVal($name, "lastUpdate", ""); my $excl = AttrVal($name, "excludeFromUpload", ""); # excludierte Objekte (Regex) $excl =~ s/[\r\n]//gx; my @aexcl = split /,/xms, $excl; push @aexcl, $excluplddef; $excl = join "|", @aexcl; my $crt = time; # current runtime ($mode, my $d) = split ":", $mode; # $d = Anzahl Tage bei Mode= nth:X my $found; my $sn = 0; for my $obj (@$allref) { # Objekte und Inhalte der Ordner auslesen für add Queue find ( { wanted => sub { my $file = $File::Find::name; my $dir = $File::Find::dir; if("$file" =~ m/^$excl$/xs) { # File excludiert from Upload Log3 ($name, 3, qq{$name - Object "$file" is excluded from Upload}); return; } if($mode eq "inc" && $data{$type}{$name}{uploaded}{"$file"}{done}) { my $elapsed = ($crt - $data{$type}{$name}{uploaded}{"$file"}{ts})/86400; # verstrichene Zeit seit dem letzten Upload des Files in Tagen return if($elapsed < ($crt-(stat($file))[9])/86400); } if($mode eq "nth") { return if(-M $file > $d); } if(-f $file && -r $file) { $sn++; $dir =~ s/^\.//x; $dir =~ s/\/$//x; $found->{$sn}{lfile} = "$file"; $found->{$sn}{ldir} = "$dir"; $found->{$sn}{mtime} = (stat $file)[9]; $found->{$sn}{crtime} = (stat $file)[10]; } }, no_chdir => 1 }, $obj ); } return $found; } #################################################################################################### # Abbruchroutine BlockingCall #################################################################################################### sub blockingTimeout { my ($hash,$cause) = @_; my $name = $hash->{NAME}; $cause = $cause // "Timeout: process terminated"; Log3 ($name, 1, "$name -> BlockingCall $hash->{HELPER}{RUNNING_PID}{fn} pid:$hash->{HELPER}{RUNNING_PID}{pid} $cause"); setReadingErrorState ($hash, $cause); delete($hash->{HELPER}{RUNNING_PID}); checkSendRetry ($name, 0, $queueStartFn); return; } ############################################################################################# # liefert die Informationen der bisherigen Uploads ############################################################################################# sub listUploadsDone { my $hash = shift; my $name = $hash->{NAME}; my $type = $hash->{TYPE}; if (!keys %{$data{$type}{$name}{uploaded}}) { return qq{No uploads have been made so far.}; } my $out = ""; $out .= "
Date & Time of last successful Uploads done to Synology Diskstation
"; $out .= ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; $out .= ""; my $i = 0; for my $idx (sort keys %{$data{$type}{$name}{uploaded}}) { my $ds = $data{$type}{$name}{uploaded}{"$idx"}{done}; next if(!$ds); my $ts = $data{$type}{$name}{uploaded}{"$idx"}{ts}; my $ro = $data{$type}{$name}{uploaded}{"$idx"}{remobj}; $ds = "success"; $ts = FmtDateTime($ts); if ($i & 1) { # $i ist ungerade $out .= ""; } else { $out .= ""; } $i++; $out .= ""; $out .= ""; $out .= ""; $out .= ""; } $out .= ""; $out .= "
local Object remote Object Date / Time
$idx $ro $ts
"; $out .= "
"; $out .= ""; return $out; } 1; =pod =item summary Module to integrate Synology File Station =item summary_DE Modul zur Integration der Synology File Station =begin html

SSFile

    This module is used to integrate the Synology File Station into FHEM. The SSFile module is based on functions of the Synology File Station API.

    The connection to the Synology server is done via a session ID after successful login. Requests/queries of the server are stored internally in a queue and processed sequentially. If the server is temporarily unavailable, the stored queries are processed as soon as the connection to the server is available again.

    Preparation

      As a basic requirement, the Synology File Station Package must be installed on the disk station.
      The credentials of the used user are later assigned to the created device via a set credentials command. created.

    Definition

      The definition is made with:

        define <Name> SSFile <ServerAddr> [<Port>] [<Protocol>]

      The parameters describe in detail:

      Name the name of the new device in FHEM
      ServerAddr the IP address of the Synology DS. Note: If the DNS name is used instead of IP address, the attribute dnsServer should be set in the global Device !
      Port optional - port of Synology DS (default: 5000).
      Protocol optional - protocol to communicate with the DS, http or https (default: http).


      Examples:
            define SynBackup SSFile 192.168.2.10 
            define SynBackup SSFile 192.168.2.10 5001 https 
            # creates a SSFile device with default port (5000/http) or https with port 5001.
           
      After defining a device, only the set command credentials is available. This command first makes the access parameters known to the Device.

    Set

      • credentials <user> <password>
        Saves the credentials.

      • deleteUploadsDone
        Deletes the history of all successfully executed uploads to Synology Diskstation.

      • deleteRemoteObject "<File>[,<File>,...]" | "<Folder>[,<Folder>,...]" [<args>]
        Deletes the specified files or directories on Synology Diskstation. Multiple objects are to be separated by commas. Directories are to be entered without "/" at the end. All specified objects are to be enclosed in " altogether.

        Optionally can be specified as <args>:
          recursive= true: Delete files within a folder recursively. (default)
          false: Delete only first level file/folder. If a folder to be deleted contains a file, an error will occur because the folder cannot be deleted directly.

        Examples:
        set <Name> deleteRemoteObject "/backup/Carport-20200625-1147065130.jpg"
        set <Name> deleteRemoteObject "/backup/log,/backup/cookie - old.txt"
        set <Name> deleteRemoteObject "/backup/log/archive" recursive=false

      • Download "<File>[,<File>,...]" | "<Folder>[,<Folder>,...]" [<args>]
        Transfers the specified file(s) or folder(s) from Synology Diskstation to the destination. If a folder is specified, it or the contents will be saved compressed in zip format to a file. Without further specification the source object will be stored in the FHEM root directory (usually /opt/fhem), depending on the setting of the global attribute modpath, stored with identical name.

        Optionally specify:
          dest= <filename>: the object will be saved with new name in default path
          <path/filename>: the object will be saved with new name in the given path
          <path/>: the object will be saved with original name in the specified path. Important: the path must end with a "/".

        All specified objects must be enclosed in " altogether.

        Examples:
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg"
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg" dest=carport.jpg
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg" dest=./log/carport.jpg
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg" dest=./log/
        set <Name> Download "/temp/applications 2020,/backup/Carport-20200625-1147065130.jpg"
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg,/Temp/card.txt" dest=/opt/

      • listQueue
        Shows all entries in the send queue. The queue is normally only filled for a short time, but in case of a problem it can contain contain entries permanently. This allows an error that occurred during a polling task to be determined and assigned be assigned.

      • listUploadsDone
        Displays a table with date/time, source file and destination object of all successfully executed uploads to Synology Diskstation.

      • logout
        The user is logged out and the session is ended with .

      • prepareDownload "<File>[,<File>,...]" | "<Folder>[,<Folder>,...]" [<args>]
        Identical to the "Download" command. The download of the files/folders from the Synology Diskstation is however not immediately but the entries are only put into the send queue. To start the transfer, the command

        .
          set <Name> startQueue

        must be executed.

      • prepareUpload "<File>[,<File>,...]" | "<Folder>[,<Folder>,...]" <args>
        Identical to the "Upload" command. However, the transfer of the files to the Synology Diskstation is not immediately but the entries are only put into the send queue. Finally, to start the transfer, the command

        .
          set <Name> startQueue

        must be executed.

      • purgeQueue
        Purges entries in the send queue. Several options are available depending on the situation:

        .
          -all- deletes all entries present in the send queue
          -permError- deletes all entries that are excluded from further processing due to a permanent error
          <Index> deletes a unique entry from the send queue

      • startQueue
        The processing of the entries in the send queue is started. For most commands, the processing of the send queue is started is started implicitly.

      • Upload "<File>[,<File>,...]" | "<Folder>[,<Folder>,...]" <args>
        Transfers the specified local file(s)/folder(s) to Synology Diskstation. In the dest argument, specify the destination directory on the Synology DiskStation. The path of the local file(s)/folder(s) to be transferred can be specified as an absolute or relative path to the FHEM global modpath can be specified.
        Files and folder contents are read out in the standard including Subordner and transferred to the Destination structure receiving. Files can contain wildcards (*.) to upload only certain files.
        Subdirectories are created by default in the destination if they do not exist.
        All specified objects are to be enclosed in " altogether.

        Mandatory arguments:
          dest= <Folder>: destination path to store the files in the Synology filesystem (the path starts with a shared folder and ends without "/")
          POSIX % wildcards can be specified.

        Optional arguments:
          ow= true: the file will be overwritten if present in the destination path (default), false: the file will not be overwritten
          cdir= true: create parent folder(s) if not present. (default), false: do not create parent folder(s)
          mode= full: all objects except those specified in the excludeFromUpload attribute will be included (default)
          inc: only new objects and objects that have changed after the last upload will be considered
          nth:<days>: only objects newer than <days> are considered (fractional numbers are allowed, e.g. 3.6)
          struc= true: all objects including their directory structure will be stored in the destination path (default)
          false: all objects will be saved without their original directory structure in the destination path

        Examples:
        set <Name> Upload "./text.txt" dest=/home/upload
        set <Name> Upload "/opt/fhem/old data.txt" dest=/home/upload ow=false
        set <Name> Upload "./archive new 2020.txt" dest=/home/upload
        set <Name> Upload "./log" dest=/home/upload mode=inc struc=false
        set <Name> Upload "./log/*.txt,./log/archive/fhem-2019-12*.*" dest=/home/upload mode=full
        set <Name> Upload "./log" dest=/home/upload/%Y_%m_%d_%H_%M_%S mode=full struc=false
        set <Name> Upload "./" dest=/home/upload mode=inc
        set <Name> Upload "/opt/fhem/fhem.pl,./www/images/PlotToChat.png,./log/fhem-2020-10-41.log" dest=/home/upload

    Get

      • apiInfo
        Gets the API information of Synology File Station and opens a popup with this information.

      • remoteFileInfo "<File>[,<File>,...]"
        Lists information from one or more files on Synology Diskstation separated by a comma ",". All objects must be enclosed in " altogether.

        Example:
        get <Name> remoteFileInfo "/ApplicationBackup/export.csv,/ApplicationBackup/export_2020_09_25.csv"


      • remoteFolderList [<args>]
        Lists all shared folders or files in a specified folder on Synology Diskstation and creates detailed file information. Without argument, all shared root folders are listed. A folder path and additional options can be specified.

        sort_direction= asc: sort ascending, desc: sort descending
        onlywritable= true: list writable shared folders, false: list writable and read-only shared folders
        limit= integer: number of files requested. 0 - show all files in a given folder (default).
        pattern= Pattern to filter files to display or file extensions. Multiple patterns can be specified separated by ",".
        filetype= file: list files only, dir: list folders only, all: list files and folders

        Objects with spaces in their names should be enclosed in ".

        Examples:
        get <name> remoteFolderList /home
        get <Name> remoteFolderList "/home/30_house & construction"
        get <Name> remoteFolderList "/home/30_house & construction" filetype=file limit=2
        get <Name> remoteFolderList "/home/30_house & construction" sort_direction=desc
        get <Name> remoteFolderList /home/lyrics pattern=doc,txt


      • storedCredentials
        Shows the stored user/password combination.


      • versionNotes
        Shows information and help about the module.


    Attributes

      • additionalInfo
        Sets the additional properties to display when retrieving file or directory information.

      • excludeFromUpload
        The entered files or directories will be excluded from upload (transfer to Synology Diskstation). The entries are evaluated as regex. Multiple objects must be separated by commas.
        Note: Files/directories with "@" in the name are excluded from upload by default.

        Example:
        attr <name> excludeFromUpload ./FHEM/FhemUtils/cacheSSCam.*,./www/SVGcache.*

      • interval <seconds>
        Automatically start the internal queue every X seconds. All entries contained in the queue at the start time. (e.g. inserted with prepareUpload) are processed. If "0" is specified, no automatic start is executed. (default)

      • loginRetries
        Number of attempts for the initial user login.
        (default: 3)

      • noAsyncFillQueue
        Filling the upload queue can be very time consuming depending on the number of files/folders to upload and can potentially block FHEM potentially block. For this reason, the queue is filled in a concurrent process.
        If only few entries are to be processed or with very fast systems can be renounced by setting this attribute the use of the additional process. Use of the additional process can be waived.
        (default: 0)

      • showPassInLog
        If "1", the password or SID will be shown in the log.
        (default: 0)

      • timeout <seconds>
        Timeout for communication with the File Station API in seconds.
        (default: 20)

=end html =begin html_DE

SSFile

    Mit diesem Modul erfolgt die Integration der Synology File Station in FHEM. Das Modul SSFile basiert auf Funktionen der Synology File Station API.

    Die Verbindung zum Synology Server erfolgt über eine Session ID nach erfolgreichem Login. Anforderungen/Abfragen des Servers werden intern in einer Queue gespeichert und sequentiell abgearbeitet. Steht der Server temporär nicht zur Verfügung, werden die gespeicherten Abfragen abgearbeitet sobald die Verbindung zum Server wieder verfügbar ist.

    Vorbereitung

      Als Grundvoraussetzung muss das Synology File Station Package auf der Diskstation installiert sein.
      Die Zugangsdaten des verwendeten Users werden später über ein Set credentials Kommando dem angelegten Device zugewiesen.

    Definition

      Die Definition erfolgt mit:

        define <Name> SSFile <ServerAddr> [<Port>] [<Protocol>]

      Die Parameter beschreiben im Einzelnen:

      Name der Name des neuen Devices in FHEM
      ServerAddr die IP-Addresse der Synology DS. Hinweis: Wird der DNS-Name statt IP-Adresse verwendet, sollte das Attribut dnsServer im global Device gesetzt werden !
      Port optional - Port der Synology DS (default: 5000).
      Protocol optional - Protokoll zur Kommunikation mit der DS, http oder https (default: http).


      Beispiele:
            define SynBackup SSFile 192.168.2.10 
            define SynBackup SSFile 192.168.2.10 5001 https 
            # erstellt ein SSFile-Device mit Standardport (5000/http) bzw. https mit Port 5001
           
      Nach der Definition eines Devices steht nur der set-Befehl credentials zur Verfügung. Mit diesem Befehl werden zunächst die Zugangsparameter dem Device bekannt gemacht.

    Set

      • credentials <User> <Passwort>
        Speichert die Zugangsdaten.

      • deleteUploadsDone
        Löscht die Historie aller erfolgreich ausgeführten Uploads zur Synology Diskstation.

      • deleteRemoteObject "<File>[,<File>,...]" | "<Ordner>[,<Ordner>,...]" [<args>]
        Löscht die angegebenen Files oder Verzeichnisse auf der Synology Diskstation. Mehrere Objekte sind durch Komma zu trennen. Verzeichnissse sind ohne "/" am Ende einzugeben. Alle angegebenen Objekte sind insgesamt in " einzuschließen.

        Optional kann als <args> angegeben werden:
          recursive= true: Dateien innerhalb eines Ordners rekursiv löschen. (default)
          false: Nur erste Ebene Datei/Ordner löschen. Wenn ein zu löschender Ordner eine Datei enthält, wird ein Fehler auftreten, weil der Ordner nicht direkt gelöscht werden kann.

        Beispiele:
        set <Name> deleteRemoteObject "/backup/Carport-20200625-1147065130.jpg"
        set <Name> deleteRemoteObject "/backup/log,/backup/cookie - old.txt"
        set <Name> deleteRemoteObject "/backup/log/archive" recursive=false

      • Download "<File>[,<File>,...]" | "<Ordner>[,<Ordner>,...]" [<args>]
        Überträgt das(die) angegebene(n) File(s) oder Ordner von der Synology Diskstation zur Destination. Ist ein Ordner angegeben, wird er bzw. der Inhalt im Zip-Format komprimiert in einer Datei gespeichert. Ohne weitere Angaben wird das Quellobjekt im FHEM Root-Verzeichnis (üblicherweise /opt/fhem), abhängig von der Einstellung des globalen Attributs modpath, mit identischem Namen gespeichert.

        Optional kann angegeben werden:
          dest= <Filename>: das Objekt wird mit neuem Namen im default Pfad gespeichert
          <Pfad/Filename>: das Objekt wird mit neuem Namen im angegebenen Pfad gespeichert
          <Pfad/>: das Objekt wird mit ursprünglichen Namen im angegebenen Pfad gespeichert. Wichtig: der Pfad muß mit einem "/" enden.

        Alle angegebenen Objekte sind insgesamt in " einzuschließen.

        Beispiele:
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg"
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg" dest=carport.jpg
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg" dest=./log/carport.jpg
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg" dest=./log/
        set <Name> Download "/Temp/Anträge 2020,/backup/Carport-20200625-1147065130.jpg"
        set <Name> Download "/backup/Carport-20200625-1147065130.jpg,/Temp/card.txt" dest=/opt/

      • listQueue
        Zeigt alle Einträge in der Sendequeue. Die Queue ist normalerweise nur kurz gefüllt, kann aber im Problemfall dauerhaft Einträge enthalten. Dadurch kann ein bei einer Abrufaufgabe aufgetretener Fehler ermittelt und zugeordnet werden.

      • listUploadsDone
        Zeigt eine Tabelle mit Datum/Zeit, Quelldatei und Zielobjekt aller erfolgreich ausgeführten Uploads zur Synology Diskstation.

      • logout
        Der User wird ausgeloggt und die Session mit beendet.

      • prepareDownload "<File>[,<File>,...]" | "<Ordner>[,<Ordner>,...]" [<args>]
        Identisch zum "Download" Befehl. Der Download der Files/Ordner von der Synology Diskstation wird allerdings nicht sofort gestartet, sondern die Einträge nur in die Sendequeue gestellt. Um die Übertragung zu starten, muß abschließend der Befehl

          set <Name> startQueue

        ausgeführt werden.

      • prepareUpload "<File>[,<File>,...]" | "<Ordner>[,<Ordner>,...]" <args>
        Identisch zum "Upload" Befehl. Die Übertragung der Files zur Synology Diskstation wird allerdings nicht sofort gestartet, sondern die Einträge nur in die Sendequeue gestellt. Um die Übertragung zu starten, muß abschließend der Befehl

          set <Name> startQueue

        ausgeführt werden.

      • purgeQueue
        Löscht Einträge in der Sendequeue. Es stehen verschiedene Optionen je nach Situation zur Verfügung:

          -all- löscht alle in der Sendequeue vorhandenen Einträge
          -permError- löscht alle Einträge, die durch einen permanenten Fehler von der weiteren Verarbeitung ausgeschlossen sind
          <Index> löscht einen eindeutigen Eintrag der Sendequeue

      • startQueue
        Die Abarbeitung der Einträge in der Sendequeue wird gestartet. Bei den meisten Befehlen wird die Abarbeitung der Sendequeue implizit gestartet.

      • Upload "<File>[,<File>,...]" | "<Ordner>[,<Ordner>,...]" <args>
        Überträgt das(die) angegebene(n) lokalen File(s)/Ordner zur Synology Diskstation. Im Argument dest ist das Zielverzeichnis auf der Synology Diskstation anzugeben. Der Pfad der zu übertragenden lokalen Files/Ordner kann als absoluter oder relativer Pfad zum FHEM global modpath angegeben werden.
        Dateien und Ordner-Inhalte werden im Standard inklusive Subordner ausgelesen und zur Destination Struktur erhaltend übertragen. Dateien können Wildcards (*.) enthalten um nur bestimmte Dateien hochzuladen.
        Unterverzeichnisse werden im Standard in der Destination angelegt wenn sie nicht vorhanden sind.
        Alle angegebenen Objekte sind insgesamt in " einzuschließen.

        Pflichtargumente:
          dest= <Ordner>: Zielpfad zur Speicherung der Files im Synology Filesystem (der Pfad beginnnt mit einem shared Folder und endet ohne "/")
          Es können POSIX %-Wildcards angegeben werden.

        Optionale Argumente:
          ow= true: das File wird überschrieben wenn im Ziel-Pfad vorhanden (default), false: das File wird nicht überschrieben
          cdir= true: übergeordnete(n) Ordner erstellen, falls nicht vorhanden. (default), false: übergeordnete(n) Ordner nicht erstellen
          mode= full: alle außer im Attribut excludeFromUpload angegebenen Objekte werden berücksichtigt (default)
          inc: nur neue Objekte und Objekte die sich nach dem letzten Upload verändert haben werden berücksichtigt
          nth:<Tage>: nur Objekte neuer als <Tage> werden berücksichtigt (gebrochene Zahlen sind erlaubt, z.B. 3.6)
          struc= true: alle Objekte werden inkl. ihrer Verzeichnisstruktur im Zielpfad gespeichert (default)
          false: alle Objekte werden ohne die ursprüngliche Verzeichnisstruktur im Zielpfad gespeichert

        Beispiele:
        set <Name> Upload "./text.txt" dest=/home/upload
        set <Name> Upload "/opt/fhem/old data.txt" dest=/home/upload ow=false
        set <Name> Upload "./Archiv neu 2020.txt" dest=/home/upload
        set <Name> Upload "./log" dest=/home/upload mode=inc struc=false
        set <Name> Upload "./log/*.txt,./log/archive/fhem-2019-12*.*" dest=/home/upload mode=full
        set <Name> Upload "./log" dest=/home/upload/%Y_%m_%d_%H_%M_%S mode=full struc=false
        set <Name> Upload "./" dest=/home/upload mode=inc
        set <Name> Upload "/opt/fhem/fhem.pl,./www/images/PlotToChat.png,./log/fhem-2020-10-41.log" dest=/home/upload

    Get

      • apiInfo
        Ruft die API Informationen der Synology File Station ab und öffnet ein Popup mit diesen Informationen.

      • remoteFileInfo "<File>[,<File>,...]"
        Listet Informationen von einer oder mehreren Dateien der Synology Diskstation getrennt durch ein Komma "," auf. Alle Objekte sind insgesamt in " einzuschließen.

        Beispiele:
        get <Name> remoteFileInfo "/ApplicationBackup/export.csv,/ApplicationBackup/export_2020_09_25.csv"


      • remoteFolderList [<args>]
        Listet alle freigegebenen Ordner oder Dateien in einem angegebenen Ordner der Synology Diskstation auf und erstellt detaillierte Dateiinformationen. Ohne Argument werden alle freigegebenen Wurzel-Ordner aufgelistet. Ein Ordnerpfad und zusätzliche Optionen können angegeben werden.

        sort_direction= asc: aufsteigend sortieren, desc: absteigend sortieren
        onlywritable= true: listet beschreibbarer freigegebener Ordner, false: auflisten beschreibbarer und schreibgeschützter freigegebener Ordner
        limit= Integer: Anzahl der angeforderten Dateien. 0 - alle Dateien in einem bestimmten Ordner zeigen (default).
        pattern= Muster zum Filtern von anzuzeigenden Dateien bzw. Dateiendungen. Mehrere Muster können durch "," getrennt angegeben werden.
        filetype= file: nur Dateien listen, dir: nur Ordner listen, all: Dateien und Ordner listen

        Objekte mit Leerzeichen im Namen sind in " einzuschließen.

        Beispiele:
        get <Name> remoteFolderList /home
        get <Name> remoteFolderList "/home/30_Haus & Bau"
        get <Name> remoteFolderList "/home/30_Haus & Bau" filetype=file limit=2
        get <Name> remoteFolderList "/home/30_Haus & Bau" sort_direction=desc
        get <Name> remoteFolderList /home/Lyrik pattern=doc,txt


      • storedCredentials
        Zeigt die gespeicherten User/Passwort Kombination.


      • versionNotes
        Zeigt Informationen und Hilfen zum Modul.


    Attribute

      • additionalInfo
        Legt die zusätzlich anzuzeigenden Eigenschaften beim Abruf von Datei- oder Verzeichnisinformationen fest.

      • excludeFromUpload
        Die eingetragenen Dateien oder Verzeichnisse werden vom Upload (Übertragung zur Synology Diskstation) ausgeschlossen. Die Angaben werden als Regex ausgewertet. Mehrere Objekte sind durch Komma zu trennen.
        Hinweis: Dateien/Verzeichnisse mit "@" im Namen werden per default vom Upload ausgeschlossen.

        Beispiel:
        attr <Name> excludeFromUpload ./FHEM/FhemUtils/cacheSSCam.*,./www/SVGcache.*

      • interval <Sekunden>
        Automatischer Start der internen Queue alle X Sekunden. Alle zum Startzeitpunkt in der Queue enthaltenen Einträge (z.B. mit prepareUpload eingefügt) werden abgearbeitet. Ist "0" angegeben, wird kein automatischer Start ausgeführt. (default)

      • loginRetries
        Anzahl der Versuche für das inititiale User login.
        (default: 3)

      • noAsyncFillQueue
        Das Füllen der Upload-Queue kann in Abhängigkeit der Anzahl hochzuladender Dateien/Ordner sehr zeitaufwändig sein und FHEM potentiell blockieren. Aus diesem Grund wird die Queue in einem nebenläufigen Prozess gefüllt.
        Sollen nur wenige Einträge verarbeitet werden oder bei sehr schnellen Systemen kann durch Setzen dieses Attributs auf die Verwendung des zusätzlichen Prozesses verzichtet werden.
        (default: 0)

      • showPassInLog
        Wenn "1" wird das Passwort bzw. die SID im Log angezeigt.
        (default: 0)

      • timeout <Sekunden>
        Timeout für die Kommunikation mit der File Station API in Sekunden.
        (default: 20)

=end html_DE =for :application/json;q=META.json 50_SSFile.pm { "abstract": "Integration of the Synology File Station.", "x_lang": { "de": { "abstract": "Integration der Synology File Station." } }, "keywords": [ "Synology", "Download", "Upload", "Backup", "Restore", "Filestransfer", "File Station" ], "version": "v1.1.1", "release_status": "stable", "author": [ "Heiko Maaz " ], "x_fhem_maintainer": [ "DS_Starter" ], "x_fhem_maintainer_github": [ "nasseeder1" ], "prereqs": { "runtime": { "requires": { "FHEM": 5.00918799, "perl": 5.014, "POSIX": 0, "JSON": 4.020, "Data::Dumper": 0, "MIME::Base64": 0, "Time::HiRes": 0, "HttpUtils": 0, "Encode": 0, "Encode::Guess": 0, "FHEM::SynoModules::API": 0, "FHEM::SynoModules::SMUtils": 0, "FHEM::SynoModules::ErrCodes": 0, "GPUtils": 0, "File::Find": 0, "File::Glob": 0 }, "recommends": { "FHEM::Meta": 0 }, "suggests": { } } }, "resources": { "x_wiki": { "web": "https://wiki.fhem.de/wiki/SSFile_-_Integration_der_Synology_File_Station", "title": "SSFile - Integration der Synology File Station" }, "repository": { "x_dev": { "type": "svn", "url": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter", "web": "https://svn.fhem.de/trac/browser/trunk/fhem/contrib/DS_Starter/50_SSFile.pm", "x_branch": "dev", "x_filepath": "fhem/contrib/", "x_raw": "https://svn.fhem.de/fhem/trunk/fhem/contrib/DS_Starter/50_SSFile.pm" } } } } =end :application/json;q=META.json =cut