dejsem.1.5/python/dejsem.pycharm/server.py
changeset 0 676905a3b03c
equal deleted inserted replaced
-1:000000000000 0:676905a3b03c
       
     1 #!/usr/bin/python3
       
     2 # coding=utf8
       
     3 
       
     4 import os, sys, subprocess, random, string, traceback, urllib.parse
       
     5 from os.path import join
       
     6 from d import D
       
     7 from parms import Parms
       
     8 from node import Node
       
     9 
       
    10 
       
    11 # operace v nekonečném cyklu serveru:
       
    12 #	- pro každý kanál v homedir je jedna instance Server
       
    13 #	- každá instance má k dispozici 10 portů pro LISTEN
       
    14 #	- číslování portů:
       
    15 #		- 17CCP
       
    16 #		- CC = číslo kanálu
       
    17 #		- P  = číslo portu v kanálu
       
    18 #		- port 17xx0 je vyhrazen pro rychlé synchronní operace serveru - commands
       
    19 #		- porty 17xx1-9 jsou pro long running tasks
       
    20 
       
    21 """
       
    22 	server pro určitý uživatelský kanál
       
    23 	instance vytváří a spouští main.py
       
    24 """
       
    25 class Server():
       
    26 	
       
    27 	def __init__(self, d, chan):
       
    28 		self._baseid = "{}.server[{:02d}]".format(d.debid, chan)
       
    29 		self.d = D("{}".format(self._baseid))
       
    30 
       
    31 		Parms.clientMode = False
       
    32 		Parms.sslCert = join(Parms.sslPath, "srv.pem")
       
    33 		self.d.log("ssl path: {}".format(Parms.sslPath), sev=3)
       
    34 
       
    35 		self._chan = chan
       
    36 		self._homedir = join(Parms.srv_homedir, "{:02d}".format(self._chan))
       
    37 		os.chdir(self._homedir)
       
    38 		self.d.log("working dir={}, ls: {}".format(os.getcwd(), os.listdir(), sev=4))
       
    39 		os.umask(0o077)
       
    40 		
       
    41 		self._filedir = join(self._homedir, Parms.filedir)
       
    42 
       
    43 		try:
       
    44 			self._node = Node(self.d, host='', chan=self._chan, conn=False, tryPort=False)
       
    45 		except Exception as e:
       
    46 			self.d.abend("creating network node", e)
       
    47 			sys.exit(1)
       
    48 		self.server()
       
    49 
       
    50 	def server(self):
       
    51 		# v této metodě server poslouchá na základním portu v kanálu (port 0);
       
    52 		# po připojení klienta alokuje socket a synchronně cyklicky volá metodu service() k provedení příkazu klienta;
       
    53 		# cyklus volání service() trvá dokud service() nevrátí False;
       
    54 		# smyslem cyklu před novým acceptem je dát možnost klientu provést rychlou dávku příkazů proveditelných okamžitě;
       
    55 		if not os.path.exists(Parms.clipfile):
       
    56 			os.system("touch " + Parms.clipfile)
       
    57 		if not os.path.exists(Parms.filedir):
       
    58 			os.mkdir(Parms.filedir)
       
    59 		while True:
       
    60 			try:
       
    61 				if self._node.acc(acc_TO=None):
       
    62 					while self.service(): pass
       
    63 			except KeyboardInterrupt:
       
    64 				self.d.log("accept: KeyboardInterrupt")
       
    65 				break
       
    66 		if self.d.ll(4): self.d.log("closing ssc...")
       
    67 		self._node.close_ssc()
       
    68 
       
    69 	def service(self):
       
    70 		# metoda synchronně ("blocking") dostává ze socketu příkaz klienta, provede ho
       
    71 		#   a vrací true, když chce nechat socket otevřený a false, když se může socket zavřít;
       
    72 		# socket se nechává otevřený, když má smysl provádět danou operaci v dávce;
       
    73 		#   je na klientu, aby dávka byla krátká a aby zavřel neprodleně
       
    74 		cmd = ""
       
    75 		try:
       
    76 			if self.d.ll(2): self.d.log("get command...")
       
    77 			random.getrandbits(16)
       
    78 			cmd = self._node.getcmd()	 # čtení osmi-znakového příkazu
       
    79 			if self.d.ll(5): self.d.log("cmd len={}".format(len(cmd)))
       
    80 			if not cmd: return False
       
    81 			if self.d.ll(3): self.d.log("cmd={}".format(cmd))
       
    82 			# názvy operací pull/push odpovídají pohledu klienta;
       
    83 			# operace jsou synchronní, blokují další příkazy v daném kanále a musí být v "lidském" měřítku krátké;
       
    84 			# dlouhé operace se zahajují příkazem LONGTASK, po němž se alokuje paralelní nit k provedení dlouhé operace (typicky přenosu dat);
       
    85 			if   cmd == "PULLCLIP":	# přenos posledního uloženého clipboardu na klienta
       
    86 				return self.cmd_pullclip()
       
    87 			elif cmd == "PULLHIST":	# přenos přehledu uložených clipboard entries na klienta
       
    88 				return self.cmd_pullhist()
       
    89 			elif cmd == "PULLLIST":	# přenos seznamu souborů v adresáři na klienta
       
    90 				return self.cmd_pulllist()
       
    91 			elif cmd == "PUSHCLIP":	# příjem nového clipboardu k uložení na serveru
       
    92 				return self.cmd_pushclip()
       
    93 			elif cmd == "GETPEER":	# předání uložené peer-adresy klientu
       
    94 				return self.cmd_getpeer()
       
    95 			elif cmd == "SETPEER":	# příjem peer-adresy k uložení na serveru
       
    96 				return self.cmd_setpeer()
       
    97 			elif cmd == "MOVE":		# přesun uloženého souboru/adresáře v rámci serveru
       
    98 				return self.cmd_move()
       
    99 			elif cmd == "EXPOSE":	# vystavení souboru/adresáře na http-serveru - vrací se URI path
       
   100 				return self.cmd_expose()
       
   101 			elif cmd == "HIDE":		# zakrytí souboru/adresáře pro http-server
       
   102 				return self.cmd_hide()
       
   103 			elif cmd == "DELETE":	# rekurzivní výmaz souboru/adresáře na serveru
       
   104 				return self.cmd_delete()
       
   105 			elif cmd == "CREATDIR":	# založení adresáře na serveru
       
   106 				return self.cmd_createdir()
       
   107 			elif cmd == "FREE":		# předání informace o volném prostoru na serveru
       
   108 				return self.cmd_freespace()
       
   109 			elif cmd == "RECKON":	# předání informace o velikost souboru/adresáře
       
   110 				return self.cmd_reckon()
       
   111 			elif cmd == "LONGTASK":	# zahájení operace v jiném procesu na jiném portu
       
   112 				return self.cmd_longtask()
       
   113 			elif cmd == "HACK":
       
   114 				self.d.log("hack")
       
   115 				return False
       
   116 			else:
       
   117 				if self.d.ll(4): self.d.log("unknown command")
       
   118 				self._node.close_sc()
       
   119 				return False
       
   120 		except KeyboardInterrupt:
       
   121 			raise
       
   122 		except Exception as e:
       
   123 			# if self.d.ll(4): self.d.log("service {} aborted: {}: {}".format(cmd, e.__class__.__name__, str(e)))
       
   124 			if self.d.ll(4): self.d.log("service {} aborted: {}".format(cmd, e))
       
   125 			traceback.print_tb(sys.exc_info()[2])
       
   126 			self._node.close_sc()
       
   127 			return False
       
   128 
       
   129 	def cmd_longtask(self):
       
   130 		"""
       
   131 		najde volný port 1-9, pošle ho klientovi, forkne se do paralelního procesu v němž
       
   132 			čeká na připojení klienta, čte příkaz k operaci a provádí operaci
       
   133 		- operace se nedávkují, na konci procesu se socket a serversocket zavírají a port uvolňuje
       
   134 		- ssc, port = self._node.bindtrynext(Parms.srvhost)
       
   135 		"""
       
   136 		try:
       
   137 			longtaskNode = Node(self.d, host='', chan=self._chan, tryPort=True, conn=False)
       
   138 		except Node.AllPortsBusy:
       
   139 			self._node.sendport(0)
       
   140 			return False
       
   141 		self._node.sendport(longtaskNode.port)
       
   142 		self._node.close_sc()
       
   143 		if os.fork():
       
   144 			return False
       
   145 		# subprocess --------------
       
   146 		self._node = longtaskNode
       
   147 		self.d.debid = "{} long task[{:d}]".format(self._baseid, self._node.port)
       
   148 		random.seed()   # je potřeba se odstřihnout od random-sekvence hlavního procesu
       
   149 		try:
       
   150 			if self._node.acc(acc_TO=Parms.long_run_accept_timeout):
       
   151 				while self.longserv(): pass	 # cykluj, dokud je longserv positivní
       
   152 		except Exception as e:
       
   153 			self.d.abendMsg("long task accept", e=e)
       
   154 		self._node.close_sc()
       
   155 		self._node.close_ssc()
       
   156 		os._exit(0)
       
   157 
       
   158 	def longserv(self):
       
   159 		cmd = self._node.getcmd()
       
   160 		if not cmd: return False
       
   161 		try:
       
   162 			if self.d.ll(4): self.d.log("longtask cmd: " + cmd)
       
   163 			# názvy operací pull/push odpovídají pohledu klienta
       
   164 			if   cmd == "PUSHFILE":	 # rekurzivní příjem souboru/adresáře k uložení na serveru
       
   165 				return self.cmd_pushfile()
       
   166 			elif cmd == "PUSHFIEX":	 # rekurzivní příjem souboru/adresáře k vystavení na http serveru
       
   167 				return self.cmd_pushFileExpose()
       
   168 			elif cmd == "PUSHSTEX":	 # příjem streamu k vystavení na http serveru
       
   169 				return self.cmd_pushStreamExpose()
       
   170 			elif cmd == "PULLFILE":	 # rekurzivní odeslání obsahu souboru/adresáře
       
   171 				return self.cmd_pullfile()
       
   172 			elif cmd == "PULLCLIP":	 # využije se pro dávkovou synchronizaci historie clipboardu
       
   173 				return self.cmd_pullclip()
       
   174 			elif cmd == "PULLHIST":	 # využije se pro dávkovou synchronizaci historie clipboardu
       
   175 				return self.cmd_pullhist()
       
   176 			elif cmd == "ECHO":
       
   177 				return self.ee(cmd)
       
   178 			elif cmd == "ECHOECHO":
       
   179 				return self.ee(cmd)
       
   180 			elif cmd == "HACK":
       
   181 				return self.rand(6)
       
   182 			else:
       
   183 				return False
       
   184 		except Exception as e:
       
   185 			self.d.abendMsg("long task action {}".format(cmd), e=e)
       
   186 			return False
       
   187 
       
   188 	def rand(self, n):
       
   189 		for i in range(n):
       
   190 			self.d.log("{:04x}".format(random.getrandbits(16)))
       
   191 		return False
       
   192 
       
   193 	def ee(self, cmd):
       
   194 		self.d.log("ECHO.004: cmd={}".format(cmd))
       
   195 		while cmd == "ECHO____" or cmd == "ECHOECHO":
       
   196 			self.d.log("echoing...")
       
   197 			self._node.putstr("ECHO")
       
   198 			if cmd == "ECHOECHO": self.d.log("echo, got={}".format(self._node.getfn()))
       
   199 			self.d.log("waiting for cmd")
       
   200 			cmd = self._node.getcmd()
       
   201 			self.d.log("cmd got={}".format(cmd))
       
   202 		self.d.log("waiting for close")
       
   203 		self._node.get(1)  # wait for other side close
       
   204 
       
   205 	def cmd_pullfile(self):
       
   206 		# operace download, v níž může klient dávkovat příkazy PULLFILE ke stažení objektů
       
   207 		cmd = "PULLFILE"
       
   208 		while cmd == "PULLFILE":
       
   209 			req = self.formpath(self._node.getfn())
       
   210 			if req:
       
   211 				fp = os.path.normpath(join(Parms.filedir, req))
       
   212 				if os.path.exists(fp):
       
   213 					prefix = os.path.dirname(fp)
       
   214 					if self.d.ll(3): self.d.log("pullfile request for '{}': starting...".format(fp))
       
   215 					self.pullfilerecurse(fp, prefix)
       
   216 				else:
       
   217 					if self.d.ll(3): self.d.log("pullfile request for '{}': not found".format(fp))
       
   218 			self._node.sendEOD()  # end of recursive subtree of files
       
   219 			cmd = self._node.getcmd()	# another PULLFILE is expected here
       
   220 		self._node.getcmd()  # wait for other side close
       
   221 		return False	# konec dávky
       
   222 
       
   223 	def pullfilerecurse(self, fp, prefix):
       
   224 		if os.path.isdir(fp):
       
   225 			for cwd, void, files in os.walk(fp, topdown=False):
       
   226 				for entry in files:
       
   227 					self.pullfilerecurse(join(cwd, entry), prefix)
       
   228 				self._node.putfileinfo(cwd, os.path.relpath(cwd, start=prefix))
       
   229 		else:
       
   230 			self._node.putfileinfo(fp, os.path.relpath(fp, start=prefix))
       
   231 			with open(fp, mode='rb') as f:
       
   232 				g = self._node.genput()
       
   233 				g.send(None)
       
   234 				data = f.read(Parms.bufSize)
       
   235 				while data:
       
   236 					try:
       
   237 						g.send(data)
       
   238 						data = f.read(Parms.bufSize)
       
   239 					except Exception:
       
   240 						break
       
   241 				g.close()
       
   242 		if self.d.ll(3): self.d.log("PULLFILE: entry {} sent".format(fp))
       
   243 
       
   244 	def cmd_pushfile(self):
       
   245 		# předpokládá se, že subadresáře, do nichž se přijímá, jsou vždycky writable, vznikly uploadem
       
   246 		if self.d.ll(4): self.d.log("push file start")
       
   247 		cmd = "PUSHFILE"
       
   248 		while cmd == "PUSHFILE":
       
   249 			req = self._node.getfn()	# relativní cesta
       
   250 			while len(req) > 0:			# rekurzivní načtení stromu - končí prázdným req
       
   251 				req = self.formpath(req)
       
   252 				uploadPath = join(Parms.filedir, req)
       
   253 				size = self._node.getnum()
       
   254 				timestamp = self._node.getnum()
       
   255 				if self.d.ll(4): self.d.log("push: req={} to=[{}, dir={}]...".format(req, uploadPath, size == -1))
       
   256 				if size < 0:
       
   257 					self._node.receive_dir(uploadPath, size, timestamp)
       
   258 				else:
       
   259 					self._node.receive_file(uploadPath, size, timestamp)
       
   260 				req = self._node.getfn()
       
   261 			cmd = self._node.getcmd()		# PUSHFILE or BATCHEND is expected here
       
   262 			if self.d.ll(4): self.d.log("cmd_pushfile(), iterace v dávce, cmd={}".format(cmd))
       
   263 		self._node.putnum(0)  				# client awaits end of transfer confirmation
       
   264 		if self.d.ll(4): self.d.log("push file finished")
       
   265 		return False	# konec dávky
       
   266 
       
   267 	def cmd_pushFileExpose(self):
       
   268 		"""
       
   269 		Upload and Expose to HTTP
       
   270 		● klient posílá na pozadí objekt, který se vystaví na http-serveru
       
   271 		● objekt se ukládá do zvláštního adresáře self._exposed pod randomizovaným jménem
       
   272 		● klientovi se posílá segment "path" z výsledného URL, URL si zkomletuje klient
       
   273 		● metoda vrací False, protože se expose nedávkuje
       
   274 		:return: False
       
   275 		"""
       
   276 		if self.d.ll(4): self.d.log("push and expose file start")
       
   277 		self.link_exposed()
       
   278 		req = self._node.getfn()  		# relativní cesta
       
   279 		if len(req) > 0:
       
   280 			stem, void, rest = self.formpath(req).partition('/')
       
   281 			exposedPath	= self.randomize_path(stem, self._exposed)
       
   282 			while len(req) > 0:			# rekurzivní načtení stromu - končí prázdným req
       
   283 				uploadPath = join(self._exposed, exposedPath)
       
   284 				void, void, rest = self.formpath(req).partition('/')
       
   285 				if rest: uploadPath = join(uploadPath, rest)
       
   286 				size = self._node.getnum()
       
   287 				timestamp = self._node.getnum()
       
   288 				if self.d.ll(4): self.d.log("push to '{}, dir={}'...".format(uploadPath, size == -1))
       
   289 				if size < 0:
       
   290 					self._node.receive_dir(uploadPath, size, timestamp)
       
   291 				else:
       
   292 					self._node.receive_file(uploadPath, size, timestamp)
       
   293 				req = self._node.getfn()  # relativní cesta
       
   294 			self.d.log("{} uploaded".format(exposedPath))
       
   295 			self.expose_link(exposedPath)
       
   296 			if self.d.ll(4): self.d.log("push and expose file end")
       
   297 		else:
       
   298 			self.node.putnum(0)
       
   299 		return False	# konec, expose se nedávkuje
       
   300 
       
   301 	def cmd_pushStreamExpose(self):
       
   302 		"""
       
   303 		Upload and Expose to HTTP
       
   304 		● klient posílá na pozadí stream, který se vystaví na http-serveru
       
   305 		● stream se ukládá do zvláštního adresáře self._exposed pod randomizovaným jménem
       
   306 		● klientovi se posílá segment "path" z výsledného URL, URL si zkomletuje klient
       
   307 		● metoda vrací False, protože se expose nedávkuje
       
   308 		:return: False
       
   309 		"""
       
   310 		if self.d.ll(4): self.d.log("push and expose stream start")
       
   311 		expName, origName = None, Node
       
   312 		mimeType = self._node.getstr()
       
   313 		size = self._node.getnum()
       
   314 		if size > 0:
       
   315 			self.link_exposed()
       
   316 			(type, suffix) = mimeType.split('/')
       
   317 			if not type or type == '*': type = "content"
       
   318 			expName = self.randomize_path(type, self._exposed)
       
   319 			if suffix and suffix != '*': expName += '.' + suffix
       
   320 			expPath = join(self._exposed, expName)
       
   321 			if self.d.ll(4): self.d.log("push stream to '{}'...".format(expPath))
       
   322 			self._node.receive_stream(expPath, size)
       
   323 			# subprocess.run(('touch', expPath))
       
   324 			self.expose_link(expName)
       
   325 			if self.d.ll(4): self.d.log("push stream to '{}' finished".format(expPath))
       
   326 		else:
       
   327 			"""self.node.putnum(0)"""
       
   328 		return False	# konec dávky
       
   329 
       
   330 	def cmd_pulllist(self):
       
   331 		req = self.formpath(self._node.getfn())
       
   332 		if req:
       
   333 			fp = os.path.normpath(join(Parms.filedir, req))
       
   334 			if self.d.ll(4): self.d.log("dir=" + fp)
       
   335 			if os.path.exists(fp) and os.access(fp, os.R_OK | os.X_OK):
       
   336 				if os.path.isdir(fp):
       
   337 					for entry in os.listdir(path=fp):
       
   338 						self._node.putfileinfo(join(fp, entry), entry)
       
   339 				else:
       
   340 					self._node.putfileinfo(fp, req)
       
   341 		self._node.putnum(0)
       
   342 		self._node.close_sc()
       
   343 		return False	# konec dávky
       
   344 
       
   345 	def cmd_pullclip(self):
       
   346 		fn = self._node.getfn()
       
   347 		if fn == "":
       
   348 			fp = Parms.clipfile
       
   349 		else:
       
   350 			fp = Parms.histdir + "/" + fn
       
   351 		if self.d.ll(4):
       
   352 			self.d.log("clip fp={}".format(fp))
       
   353 		if os.path.exists(fp):
       
   354 			self._node.putnum(os.path.getsize(fp))
       
   355 			with open(fp, mode="rb") as f:
       
   356 				g = self._node.genput()
       
   357 				g.send(None)
       
   358 				data = f.read(Parms.bufSize)
       
   359 				while len(data) > 0:
       
   360 					try:
       
   361 						g.send(data)
       
   362 						data = f.read(Parms.bufSize)
       
   363 					except Exception:
       
   364 						break
       
   365 				g.close()
       
   366 		return True		# případné dávkování
       
   367 
       
   368 	def cmd_pushclip(self):
       
   369 		with open(Parms.clipfile, mode='wb') as f:
       
   370 			for data in self._node.genget(size = -1):
       
   371 				f.write(data)
       
   372 		self._node.close_sc()
       
   373 		subprocess.call(("cp", "-a", Parms.clipfile, join(Parms.histdir, self.hist_fn())))
       
   374 		if self.d.ll(4): self.d.log("pushclip: {} bytes stored".format(os.path.getsize(Parms.clipfile)))
       
   375 		return False	# konec dávky
       
   376 
       
   377 	def hist_fn(self):	# random string file name
       
   378 		if not os.path.exists(Parms.histdir):
       
   379 			os.mkdir(Parms.histdir)
       
   380 		a = (string.digits + string.ascii_letters)
       
   381 		fn = ""
       
   382 		for i in list(range(5)):
       
   383 			fn += a[random.randint(0, 61)]
       
   384 		while os.path.exists(join(Parms.histdir, fn)):
       
   385 			fn = ""
       
   386 			for i in list(range(5)):
       
   387 				fn += a[random.randint(0, 61)]
       
   388 		return fn
       
   389 
       
   390 	def cmd_pullhist(self):
       
   391 		if os.path.exists(Parms.histdir) and os.path.isdir(Parms.histdir):
       
   392 			for entry in os.listdir(Parms.histdir):
       
   393 				p = join(Parms.histdir, entry)
       
   394 				self._node.putstr(entry)
       
   395 				sample = open(p, mode="rb").read(80)	# pošli vzorek max.80 z každého entry
       
   396 				self._node.putnum(len(sample))
       
   397 				self._node.put(sample)
       
   398 				self._node.putnum(os.path.getsize(p))
       
   399 				self._node.putnum(int(os.path.getmtime(p)))
       
   400 		self._node.putnum(0)	# konec streamu
       
   401 		return True
       
   402 
       
   403 	def cmd_expose(self):
       
   404 		"""
       
   405 		Expose to HTTP
       
   406 		● klient posílá jméno objektu na serveru, který se má vystavit na http-serveru
       
   407 		● jméno je cesta relativní k self._filedir
       
   408 		● objekt se symlinkuje ve zvláštním adresáři self._exposed randomizovaným jménem odvozeným ze jména objektu
       
   409 		● klientovi se posílá segment "path" z výsledného URL, URL si zkompletuje klient
       
   410 		● metoda vrací False, protože se expose nedávkuje
       
   411 		:return: False
       
   412 		"""
       
   413 		fpath = join(self._filedir, self.formpath(self._node.getfn()))	# node.getfn() je cesta relativně k self._filedir
       
   414 		self.link_exposed()
       
   415 		expName = self.randomize_path(os.path.basename(fpath), self._exposed)
       
   416 		expPath = join(self._exposed, expName)
       
   417 		subprocess.run(('ln', '-sfnr', fpath, expPath), check=True)
       
   418 		if self.d.ll(3): self.d.log("fn={}".format(expPath))
       
   419 		self.expose_link(expName)
       
   420 		return False		# expose nemůže být v dávce, protože se klientovi posílá zpátky URI path
       
   421 
       
   422 	def cmd_hide(self):
       
   423 		fpath = self.formpath(self._node.getfn())
       
   424 		if self.d.ll(3): self.d.log("fn={}".format(fpath))
       
   425 		channel = "{:02d}".format(self._chan)
       
   426 		fpath = join(Parms.srv_homedir, channel, Parms.filedir, fpath)
       
   427 		subprocess.run(('chmod', '-R', 'o-rwx', fpath))
       
   428 		subprocess.run(('find', fpath, '-name', '.htaccess', '-exec', 'rm', '-f', '{}', '+'))
       
   429 		return True		# případné dávkování
       
   430 
       
   431 	def cmd_getpeer(self):	# atavismus z doby před použitím UDP broadcast
       
   432 		host = ""
       
   433 		port = 0
       
   434 		if len(self.peer) == 2:
       
   435 			host = "10.0.2.2" if self.peer[0] == "10.0.2.15" else str(self.peer[0])
       
   436 			port = self.peer[1]
       
   437 			self.peer = []
       
   438 		self._node.putstr(host)
       
   439 		self._node.putnum(port)
       
   440 		self._node.close_sc()
       
   441 		return False
       
   442 
       
   443 	def cmd_setpeer(self):	# atavismus z doby před použitím UDP broadcast
       
   444 		self.peer = (str(self._node.getfn()), int(self._node.getnum()))
       
   445 		self._node.close_sc()
       
   446 		return False
       
   447 
       
   448 	def cmd_move(self):
       
   449 		orig = self.formpath(self._node.getfn())
       
   450 		target = self.formpath(self._node.getfn())
       
   451 		if orig and target:
       
   452 			if self.d.ll(4): self.d.log("performing mv -n {} {}".format(orig, target))
       
   453 			subprocess.call(["mv", "-n", join(Parms.filedir, orig), join(Parms.filedir, target)])
       
   454 		return True	 	# případné dávkování
       
   455 
       
   456 	def cmd_delete(self):
       
   457 		req = self.formpath(self._node.getfn())
       
   458 		if req:
       
   459 			if self.d.ll(4): self.d.log("performing rm -rf {}".format(req))
       
   460 			subprocess.call(["rm", "-rf", join(Parms.filedir, req)])
       
   461 		return True		# případné dávkování
       
   462 
       
   463 	def cmd_createdir(self):
       
   464 		req = self.formpath(self._node.getfn())
       
   465 		if req:
       
   466 			fp = join(Parms.filedir, req)
       
   467 			if self.d.ll(4): self.d.log("performing mkdir -p {}".format(fp))
       
   468 			subprocess.call(["mkdir", "-p", fp])
       
   469 		return True	 		# případné dávkování
       
   470 
       
   471 	def cmd_freespace(self):
       
   472 		# df v Debianu 7.6 nemá parametr --output   :-(
       
   473 		p = subprocess.Popen(["df", "--block-size=1", "."], stdout=subprocess.PIPE)
       
   474 		p.wait()
       
   475 		if p.returncode == 0:
       
   476 			free = int(p.stdout.readlines()[1].decode().split()[3])
       
   477 		else:
       
   478 			free = -1
       
   479 		self._node.putnum(free)
       
   480 		self._node.close_sc()
       
   481 		return True	 		# případné dávkování
       
   482 
       
   483 	def cmd_reckon(self):
       
   484 		req = self.formpath(self._node.getfn())
       
   485 		self.sendobjectsize(join(Parms.filedir, req) if req else None)
       
   486 		return True	 		# případné dávkování
       
   487 
       
   488 	def sendobjectsize(self, fp):
       
   489 		if not fp or not os.path.exists(fp):
       
   490 			size, fnum, dnum = (0, 0, 0)
       
   491 		elif os.path.isfile(fp):
       
   492 			size, fnum, dnum = (os.path.getsize(fp), 1, 0)
       
   493 		else:
       
   494 			size, fnum, dnum = self.dirsize(fp)
       
   495 		self._node.putnum(int(size))
       
   496 		self._node.putnum(int(fnum))
       
   497 		self._node.putnum(int(dnum))
       
   498 
       
   499 	def link_exposed(self):
       
   500 		"""
       
   501 		● založení adresáře pro vystavené objekty self._exposed podle Parms.exposed
       
   502 		● adresář je symlinkován z http-serveru číslem kanálu
       
   503 		● symlink z http-serveru musí vytvořit instalace nebo super-user
       
   504 		● na http-serveru se při instalaci aplikace zakládá adresář pro tuto aplikaci se symlinky na exposed dirs jednotlivých kanálů
       
   505 			● <http_server>/<appl_name>/<kanál> --> <appl_home>/<kanál>/<files>/<exposed>
       
   506 			● <http_server>/<appl_name> musí mít povoleny symlinky a povolen( instalovat .htaccess s Options Indexes
       
   507 			● http-server musí mít x-access po cestě <http_server>/<appl_name>/<kanál> --> <exposed>
       
   508 			● výšeuvedené atributy se nemůže zařídit aplikace, musí být nastaveny při instalaci nebo administrátorem
       
   509 		"""
       
   510 		self._exposed = join(self._filedir, Parms.exposed)
       
   511 		if not os.path.exists(self._exposed):
       
   512 			subprocess.run(('mkdir', '-pm771', self._exposed))
       
   513 		# os.mkdir(self._exposed, mode=0o771)
       
   514 		channel = "{:02d}".format(self._chan)
       
   515 		self._wwwhome = join(Parms.srv_wwwhomedir, channel)
       
   516 		if not os.path.exists(self._wwwhome):
       
   517 			subprocess.run(('ln', '-sfnr', self._exposed, self._wwwhome), check=True)
       
   518 
       
   519 	def expose_link(self, path):
       
   520 		"""
       
   521 		● fpath je cesta relativní k self._exposed
       
   522 		● úkolem je zařídit read-access k filům, x-access k dirs, případně .htacess v kořenu stromu
       
   523 		"""
       
   524 		orig = join(self._exposed, path)
       
   525 		self.d.log("orig={}".format(orig), sev=4)
       
   526 		try:
       
   527 			subprocess.run(('chmod', 'o+r', orig), check=True)
       
   528 			if os.path.isdir(orig):
       
   529 				self.expose_dir_tree(orig)
       
   530 			elif path.find('/') > 0:
       
   531 				self.expose_dir_path(path)
       
   532 			channel = "{:02d}".format(self._chan)
       
   533 			# uriPath = urllib.parse.quote(join(Parms.applName, channel, path))
       
   534 			uriPath = join(Parms.applName, channel, path)
       
   535 			self._node.putstr(uriPath)  # pošli URL-path klientovi
       
   536 			self.d.log("uri path {} sent".format(uriPath), sev=3)
       
   537 		except Exception as e:
       
   538 			self.d.abend("exposing file to web server", e)
       
   539 
       
   540 	def expose_dir_path(self, rel_file_path):
       
   541 		"""
       
   542 		po cestě k vystavenému souboru je potřeba nastavit x-access pro http-server
       
   543 		:param rel_file_path: cesta relativní k self._exposed
       
   544 		"""
       
   545 		base = self._exposed
       
   546 		while rel_file_path:
       
   547 			subprocess.run(('chmod', 'o=x', base))
       
   548 			(subdir, sep, rel_file_path) = rel_file_path.partition('/')
       
   549 			base = join(base, subdir)
       
   550 
       
   551 	def expose_dir_tree(self, rel_dir_path):
       
   552 		"""
       
   553 		do vystaveného stromu je třeba umístit .htaccess, po cestě nastavit x-access, ve stromu nastavit rx-access
       
   554 		:param rel_dir_path: cesta relativní k self._exposed
       
   555 		"""
       
   556 		htaccess = join(rel_dir_path, ".htaccess")
       
   557 		with open(htaccess, mode="w") as f:
       
   558 			f.write("Options Indexes")
       
   559 		subprocess.run(('chmod', 'o+r', htaccess))
       
   560 		subprocess.run(('chmod', 'o+x', rel_dir_path))
       
   561 		subprocess.run(('chmod', '-R', 'o+r', rel_dir_path))
       
   562 		for (rel_dir_path, dirs, files) in os.walk(rel_dir_path):
       
   563 			for dir in dirs:
       
   564 				dpath = join(rel_dir_path, dir)
       
   565 				subprocess.run(('chmod', 'o+x', dpath))
       
   566 
       
   567 	def randomize_path(self, fn, dirname):
       
   568 		name, void, suff = os.path.basename(fn).rpartition(".")
       
   569 		expName = ""
       
   570 		while os.path.exists(join(dirname, expName)) or expName == "":
       
   571 			uniq = "{:04x}".format(random.getrandbits(16))
       
   572 			if name:
       
   573 				expName = name + "." + uniq + "." + suff
       
   574 			else:
       
   575 				expName = suff + "." + uniq
       
   576 		return expName
       
   577 
       
   578 	def dirsize(self, fp):
       
   579 		if self.d.ll(4): self.d.log("performing du -sb '{}'".format(fp))
       
   580 		try:
       
   581 			size = subprocess.check_output(["du", "-sb", fp])[:-1].decode().split("\t")[0]
       
   582 			fnum = len(subprocess.check_output(["find", fp, "-type", "f"]).split(b'\n'))-1
       
   583 			dnum = len(subprocess.check_output(["find", fp, "-type", "d"]).split(b'\n'))-1
       
   584 		except subprocess.CalledProcessError:
       
   585 			(size, fnum, dnum) = (-1, 0, 0)
       
   586 		if self.d.ll(5): self.d.log("size={}, type={}, fnum={}. type={}, dnum={}, type={}"
       
   587 								.format(size, type(size), fnum, type(fnum), dnum, type(dnum)))
       
   588 		return (size, fnum, dnum)
       
   589 
       
   590 	def close_sc(self):
       
   591 		self._node.close_sc()
       
   592 
       
   593 	def formpath(self, path):
       
   594 		"""
       
   595 		- v žádném případě absolutní cesta
       
   596 		- vrací None, když path jde up from current
       
   597 		"""
       
   598 		if path.startswith('/'): path = path[1:]
       
   599 		p = os.path.normpath(path)
       
   600 		return None if p == '..' or p.startswith('../') else p