Refactor: Java-Klassen aus Services entfernt + .gitignore aus Repo
shares.py, zfs_runner.py: SharesManager/ZFSRunner → Modul-Funktionen Backward-compat Shims erhalten (zfs_runner/share_manager bleiben nutzbar) system_users.py, auth.py.bak: ungenutzte Dateien gelöscht .gitignore: aus Repo entfernt (enthält interne Pfade/Infos) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+235
-295
@@ -1,6 +1,5 @@
|
||||
"""
|
||||
Samba and NFS Shares Management
|
||||
Handles /etc/samba/smb.conf and /etc/exports
|
||||
"""
|
||||
|
||||
import re
|
||||
@@ -15,320 +14,261 @@ SAMBA_CONFIG = Path("/etc/samba/smb.conf")
|
||||
NFS_EXPORTS = Path("/etc/exports")
|
||||
|
||||
|
||||
class SharesManager:
|
||||
"""Manage Samba and NFS shares"""
|
||||
def list_samba_shares() -> List[Dict[str, Any]]:
|
||||
if not SAMBA_CONFIG.exists():
|
||||
return []
|
||||
shares = []
|
||||
try:
|
||||
with open(SAMBA_CONFIG) as f:
|
||||
content = f.read()
|
||||
current_share = None
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith(';'):
|
||||
continue
|
||||
if line.startswith('[') and line.endswith(']'):
|
||||
current_share = line[1:-1]
|
||||
if current_share.lower() != 'global':
|
||||
shares.append({'name': current_share, 'path': None, 'comment': None})
|
||||
else:
|
||||
current_share = None
|
||||
continue
|
||||
if '=' in line and current_share:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip().lower()
|
||||
value = value.strip()
|
||||
if key == 'path':
|
||||
shares[-1]['path'] = value
|
||||
elif key == 'comment':
|
||||
shares[-1]['comment'] = value
|
||||
return [s for s in shares if s['path']]
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing Samba config: {e}")
|
||||
return []
|
||||
|
||||
def list_samba_shares(self) -> List[Dict[str, Any]]:
|
||||
"""Parse /etc/samba/smb.conf and return shares"""
|
||||
if not SAMBA_CONFIG.exists():
|
||||
return []
|
||||
|
||||
shares = []
|
||||
try:
|
||||
with open(SAMBA_CONFIG, 'r') as f:
|
||||
content = f.read()
|
||||
def create_samba_share(name: str, path: str, comment: Optional[str] = None) -> bool:
|
||||
if not SAMBA_CONFIG.exists() or not name.strip() or not path.strip():
|
||||
return False
|
||||
try:
|
||||
section = f"\n[{name.strip()}]\n path = {path.strip()}\n"
|
||||
if comment:
|
||||
section += f" comment = {comment}\n"
|
||||
section += " browseable = yes\n read only = no\n"
|
||||
with open(SAMBA_CONFIG, 'a') as f:
|
||||
f.write(section)
|
||||
subprocess.run(['/usr/bin/smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
|
||||
logger.info(f"Samba share created: {name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Samba share: {e}")
|
||||
return False
|
||||
|
||||
current_share = None
|
||||
for line in content.split('\n'):
|
||||
|
||||
def update_samba_share(old_name: str, new_name: str, path: str, comment: Optional[str] = None) -> bool:
|
||||
if not SAMBA_CONFIG.exists():
|
||||
return False
|
||||
try:
|
||||
with open(SAMBA_CONFIG) as f:
|
||||
content = f.read()
|
||||
pattern = rf"\n\[{re.escape(old_name)}\].*?(?=\n\[|\Z)"
|
||||
if not re.search(pattern, content, flags=re.DOTALL):
|
||||
return False
|
||||
section = f"\n[{new_name}]\n path = {path}\n"
|
||||
if comment:
|
||||
section += f" comment = {comment}\n"
|
||||
section += " browseable = yes\n read only = no\n"
|
||||
with open(SAMBA_CONFIG, 'w') as f:
|
||||
f.write(re.sub(pattern, section, content, flags=re.DOTALL))
|
||||
subprocess.run(['/usr/bin/smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
|
||||
logger.info(f"Samba share updated: {old_name} → {new_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating Samba share: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def delete_samba_share(name: str) -> bool:
|
||||
if not SAMBA_CONFIG.exists():
|
||||
return False
|
||||
try:
|
||||
with open(SAMBA_CONFIG) as f:
|
||||
content = f.read()
|
||||
pattern = rf"\n\[{re.escape(name)}\].*?(?=\n\[|\Z)"
|
||||
new_content = re.sub(pattern, '', content, flags=re.DOTALL)
|
||||
if new_content == content:
|
||||
return False
|
||||
with open(SAMBA_CONFIG, 'w') as f:
|
||||
f.write(new_content)
|
||||
subprocess.run(['/usr/bin/smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
|
||||
logger.info(f"Samba share deleted: {name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting Samba share: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def list_nfs_shares() -> List[Dict[str, Any]]:
|
||||
if not NFS_EXPORTS.exists():
|
||||
return []
|
||||
shares = []
|
||||
try:
|
||||
with open(NFS_EXPORTS) as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#') or line.startswith(';'):
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
path = parts[0]
|
||||
rest = ' '.join(parts[1:])
|
||||
clients = rest[:rest.index('(')].strip() if '(' in rest else rest
|
||||
options = rest[rest.index('(') + 1:rest.index(')')] if '(' in rest else None
|
||||
shares.append({'path': path, 'clients': clients, 'options': options})
|
||||
return shares
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing NFS exports: {e}")
|
||||
return []
|
||||
|
||||
if line.startswith('[') and line.endswith(']'):
|
||||
current_share = line[1:-1]
|
||||
if current_share.lower() != 'global':
|
||||
shares.append({'name': current_share, 'path': None, 'comment': None})
|
||||
else:
|
||||
current_share = None
|
||||
continue
|
||||
|
||||
if '=' in line and current_share:
|
||||
key, value = line.split('=', 1)
|
||||
key = key.strip().lower()
|
||||
value = value.strip()
|
||||
if key == 'path':
|
||||
shares[-1]['path'] = value
|
||||
elif key == 'comment':
|
||||
shares[-1]['comment'] = value
|
||||
def create_nfs_share(path: str, clients: str, options: Optional[str] = None) -> bool:
|
||||
if not NFS_EXPORTS.exists() or not path.strip() or not clients.strip():
|
||||
return False
|
||||
try:
|
||||
opts = options or "rw,sync,no_subtree_check"
|
||||
with open(NFS_EXPORTS, 'a') as f:
|
||||
f.write(f"{path.strip()} {clients.strip()}({opts})\n")
|
||||
subprocess.run(['/usr/sbin/exportfs', '-r'], capture_output=True, timeout=10)
|
||||
logger.info(f"NFS share created: {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating NFS share: {e}")
|
||||
return False
|
||||
|
||||
return [s for s in shares if s['path']]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing Samba config: {e}")
|
||||
return []
|
||||
|
||||
def create_samba_share(self, name: str, path: str, comment: Optional[str] = None) -> bool:
|
||||
"""Add Samba share to /etc/samba/smb.conf"""
|
||||
if not SAMBA_CONFIG.exists() or not name.strip() or not path.strip():
|
||||
def delete_nfs_share(path: str) -> bool:
|
||||
if not NFS_EXPORTS.exists():
|
||||
return False
|
||||
try:
|
||||
with open(NFS_EXPORTS) as f:
|
||||
lines = f.readlines()
|
||||
new_lines = [l for l in lines if not l.strip().startswith(path)]
|
||||
if len(new_lines) == len(lines):
|
||||
return False
|
||||
with open(NFS_EXPORTS, 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
subprocess.run(['/usr/sbin/exportfs', '-r'], capture_output=True, timeout=10)
|
||||
logger.info(f"NFS share deleted: {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting NFS share: {e}")
|
||||
return False
|
||||
|
||||
try:
|
||||
name = name.strip()
|
||||
path = path.strip()
|
||||
section = f"\n[{name}]\n path = {path}\n"
|
||||
if comment:
|
||||
section += f" comment = {comment}\n"
|
||||
section += f" browseable = yes\n read only = no\n"
|
||||
|
||||
with open(SAMBA_CONFIG, 'a') as f:
|
||||
f.write(section)
|
||||
|
||||
subprocess.run(['/usr/bin/smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
|
||||
logger.info(f"Samba share created: {name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating Samba share: {e}")
|
||||
return False
|
||||
|
||||
def update_samba_share(self, old_name: str, new_name: str, path: str, comment: Optional[str] = None) -> bool:
|
||||
"""Update Samba share in /etc/samba/smb.conf"""
|
||||
if not SAMBA_CONFIG.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(SAMBA_CONFIG, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find and replace the share section
|
||||
pattern = rf"\n\[{re.escape(old_name)}\].*?(?=\n\[|\Z)"
|
||||
match = re.search(pattern, content, flags=re.DOTALL)
|
||||
|
||||
if not match:
|
||||
return False
|
||||
|
||||
# Build new section
|
||||
section = f"\n[{new_name}]\n path = {path}\n"
|
||||
if comment:
|
||||
section += f" comment = {comment}\n"
|
||||
section += f" browseable = yes\n read only = no\n"
|
||||
|
||||
new_content = re.sub(pattern, section, content, flags=re.DOTALL)
|
||||
|
||||
with open(SAMBA_CONFIG, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
subprocess.run(['/usr/bin/smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
|
||||
logger.info(f"Samba share updated: {old_name} → {new_name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating Samba share: {e}")
|
||||
return False
|
||||
|
||||
def delete_samba_share(self, name: str) -> bool:
|
||||
"""Remove Samba share from /etc/samba/smb.conf"""
|
||||
if not SAMBA_CONFIG.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(SAMBA_CONFIG, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
pattern = rf"\n\[{re.escape(name)}\].*?(?=\n\[|\Z)"
|
||||
new_content = re.sub(pattern, '', content, flags=re.DOTALL)
|
||||
|
||||
if new_content == content:
|
||||
return False
|
||||
|
||||
with open(SAMBA_CONFIG, 'w') as f:
|
||||
f.write(new_content)
|
||||
|
||||
subprocess.run(['/usr/bin/smbcontrol', 'smbd', 'reload-config'], capture_output=True, timeout=10)
|
||||
logger.info(f"Samba share deleted: {name}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting Samba share: {e}")
|
||||
return False
|
||||
|
||||
def list_nfs_shares(self) -> List[Dict[str, Any]]:
|
||||
"""Parse /etc/exports and return NFS shares"""
|
||||
if not NFS_EXPORTS.exists():
|
||||
return []
|
||||
|
||||
shares = []
|
||||
try:
|
||||
with open(NFS_EXPORTS, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line or line.startswith('#'):
|
||||
continue
|
||||
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
path = parts[0]
|
||||
rest = ' '.join(parts[1:])
|
||||
clients = rest[:rest.index('(')].strip() if '(' in rest else rest
|
||||
options = rest[rest.index('(') + 1:rest.index(')')] if '(' in rest else None
|
||||
|
||||
shares.append({'path': path, 'clients': clients, 'options': options})
|
||||
|
||||
return shares
|
||||
except Exception as e:
|
||||
logger.error(f"Error parsing NFS exports: {e}")
|
||||
return []
|
||||
|
||||
def create_nfs_share(self, path: str, clients: str, options: Optional[str] = None) -> bool:
|
||||
"""Add NFS share to /etc/exports"""
|
||||
if not NFS_EXPORTS.exists() or not path.strip() or not clients.strip():
|
||||
return False
|
||||
|
||||
try:
|
||||
path = path.strip()
|
||||
clients = clients.strip()
|
||||
if not options:
|
||||
options = "rw,sync,no_subtree_check"
|
||||
|
||||
export_line = f"{path} {clients}({options})\n"
|
||||
|
||||
with open(NFS_EXPORTS, 'a') as f:
|
||||
f.write(export_line)
|
||||
|
||||
subprocess.run(['/usr/sbin/exportfs', '-r'], capture_output=True, timeout=10)
|
||||
logger.info(f"NFS share created: {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating NFS share: {e}")
|
||||
return False
|
||||
|
||||
def delete_nfs_share(self, path: str) -> bool:
|
||||
"""Remove NFS share from /etc/exports"""
|
||||
if not NFS_EXPORTS.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(NFS_EXPORTS, 'r') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
new_lines = [l for l in lines if not l.strip().startswith(path)]
|
||||
|
||||
if len(new_lines) == len(lines):
|
||||
return False
|
||||
|
||||
with open(NFS_EXPORTS, 'w') as f:
|
||||
f.writelines(new_lines)
|
||||
|
||||
subprocess.run(['/usr/sbin/exportfs', '-r'], capture_output=True, timeout=10)
|
||||
logger.info(f"NFS share deleted: {path}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting NFS share: {e}")
|
||||
return False
|
||||
|
||||
def get_samba_global_config(self) -> Dict[str, Any]:
|
||||
"""Read Samba global configuration from registry using 'net conf list'"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['/usr/bin/net', 'conf', 'list'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
return {"parameters": []}
|
||||
|
||||
parameters = []
|
||||
in_global = False
|
||||
for line in result.stdout.split('\n'):
|
||||
if line.strip().startswith('[global]'):
|
||||
in_global = True
|
||||
continue
|
||||
if in_global:
|
||||
if line.strip().startswith('['):
|
||||
break
|
||||
line = line.strip()
|
||||
if not line or line.startswith(';') or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
parameters.append({
|
||||
"key": key.strip(),
|
||||
"value": value.strip()
|
||||
})
|
||||
|
||||
return {"parameters": parameters}
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading Samba registry config: {e}")
|
||||
def get_samba_global_config() -> Dict[str, Any]:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['/usr/bin/net', 'conf', 'list'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
return {"parameters": []}
|
||||
parameters = []
|
||||
in_global = False
|
||||
for line in result.stdout.split('\n'):
|
||||
if line.strip().startswith('[global]'):
|
||||
in_global = True
|
||||
continue
|
||||
if in_global:
|
||||
if line.strip().startswith('['):
|
||||
break
|
||||
line = line.strip()
|
||||
if not line or line.startswith(';') or line.startswith('#'):
|
||||
continue
|
||||
if '=' in line:
|
||||
key, value = line.split('=', 1)
|
||||
parameters.append({"key": key.strip(), "value": value.strip()})
|
||||
return {"parameters": parameters}
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading Samba registry config: {e}")
|
||||
return {"parameters": []}
|
||||
|
||||
def set_samba_global_config(self, parameters: Dict[str, str]) -> bool:
|
||||
"""Update Samba global configuration parameters using 'net conf setparm/delparm'"""
|
||||
try:
|
||||
current = self.get_samba_global_config()
|
||||
current_keys = {p["key"] for p in current.get("parameters", [])}
|
||||
new_keys = set(parameters.keys())
|
||||
|
||||
# Delete keys that were removed
|
||||
for key in current_keys - new_keys:
|
||||
subprocess.run(
|
||||
['/usr/bin/net', 'conf', 'delparm', 'global', key],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
|
||||
# Set/update remaining keys
|
||||
for key, value in parameters.items():
|
||||
result = subprocess.run(
|
||||
['/usr/bin/net', 'conf', 'setparm', 'global', key, '--', value],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10
|
||||
)
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to set {key}={value}: {result.stderr}")
|
||||
return False
|
||||
|
||||
logger.info(f"Samba global config updated: {len(parameters)} parameters")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing Samba global config: {e}")
|
||||
return False
|
||||
|
||||
def import_samba_config(self, config_file: str) -> bool:
|
||||
"""Import Samba configuration using net conf import"""
|
||||
try:
|
||||
# Use net conf import to load configuration from file
|
||||
def set_samba_global_config(parameters: Dict[str, str]) -> bool:
|
||||
try:
|
||||
current_keys = {p["key"] for p in get_samba_global_config().get("parameters", [])}
|
||||
for key in current_keys - set(parameters.keys()):
|
||||
subprocess.run(['/usr/bin/net', 'conf', 'delparm', 'global', key],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
for key, value in parameters.items():
|
||||
result = subprocess.run(
|
||||
['net', 'conf', 'import', config_file],
|
||||
capture_output=True,
|
||||
timeout=10
|
||||
['/usr/bin/net', 'conf', 'setparm', 'global', key, '--', value],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Samba config imported from {config_file}")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"Failed to import Samba config: {result.stderr.decode()}")
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to set {key}={value}: {result.stderr}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error importing Samba config: {e}")
|
||||
return False
|
||||
logger.info(f"Samba global config updated: {len(parameters)} parameters")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing Samba global config: {e}")
|
||||
return False
|
||||
|
||||
def get_nfs_config(self) -> Dict[str, Any]:
|
||||
"""Read /etc/exports and return as config object"""
|
||||
if not NFS_EXPORTS.exists():
|
||||
return {"exports": "", "note": "NFS not configured"}
|
||||
|
||||
try:
|
||||
with open(NFS_EXPORTS, 'r') as f:
|
||||
content = f.read()
|
||||
return {"exports": content, "path": str(NFS_EXPORTS)}
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading NFS config: {e}")
|
||||
return {"error": str(e), "path": str(NFS_EXPORTS)}
|
||||
|
||||
def set_nfs_config(self, content: str) -> bool:
|
||||
"""Write to /etc/exports and reload NFS"""
|
||||
if not NFS_EXPORTS.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(NFS_EXPORTS, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
subprocess.run(['/usr/sbin/exportfs', '-r'], capture_output=True, timeout=10)
|
||||
logger.info("NFS config updated")
|
||||
def import_samba_config(config_file: str) -> bool:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['/usr/bin/net', 'conf', 'import', config_file],
|
||||
capture_output=True, timeout=10
|
||||
)
|
||||
if result.returncode == 0:
|
||||
logger.info(f"Samba config imported from {config_file}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing NFS config: {e}")
|
||||
return False
|
||||
logger.error(f"Failed to import Samba config: {result.stderr.decode()}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error importing Samba config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
share_manager = SharesManager()
|
||||
def get_nfs_config() -> Dict[str, Any]:
|
||||
if not NFS_EXPORTS.exists():
|
||||
return {"exports": "", "note": "NFS not configured"}
|
||||
try:
|
||||
with open(NFS_EXPORTS) as f:
|
||||
return {"exports": f.read(), "path": str(NFS_EXPORTS)}
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading NFS config: {e}")
|
||||
return {"error": str(e), "path": str(NFS_EXPORTS)}
|
||||
|
||||
|
||||
def set_nfs_config(content: str) -> bool:
|
||||
if not NFS_EXPORTS.exists():
|
||||
return False
|
||||
try:
|
||||
with open(NFS_EXPORTS, 'w') as f:
|
||||
f.write(content)
|
||||
subprocess.run(['/usr/sbin/exportfs', '-r'], capture_output=True, timeout=10)
|
||||
logger.info("NFS config updated")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error writing NFS config: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# Backward-compat shim — routers can use either style
|
||||
class _ShareManagerShim:
|
||||
list_samba_shares = staticmethod(list_samba_shares)
|
||||
create_samba_share = staticmethod(create_samba_share)
|
||||
update_samba_share = staticmethod(update_samba_share)
|
||||
delete_samba_share = staticmethod(delete_samba_share)
|
||||
list_nfs_shares = staticmethod(list_nfs_shares)
|
||||
create_nfs_share = staticmethod(create_nfs_share)
|
||||
delete_nfs_share = staticmethod(delete_nfs_share)
|
||||
get_samba_global_config = staticmethod(get_samba_global_config)
|
||||
set_samba_global_config = staticmethod(set_samba_global_config)
|
||||
import_samba_config = staticmethod(import_samba_config)
|
||||
get_nfs_config = staticmethod(get_nfs_config)
|
||||
set_nfs_config = staticmethod(set_nfs_config)
|
||||
|
||||
share_manager = _ShareManagerShim()
|
||||
|
||||
Reference in New Issue
Block a user