diff --git a/README.rst b/README.rst index fa07decb..2c834ee5 100644 --- a/README.rst +++ b/README.rst @@ -92,6 +92,7 @@ The ``SynologyDSM`` class can also ``update()`` all APIs at once. print("Temp. warning: " + str(api.information.temperature_warn)) print("Uptime: " + str(api.information.uptime)) print("Full DSM version:" + str(api.information.version_string)) + print("--") print("=== Utilisation ===") api.utilisation.update() @@ -99,13 +100,15 @@ The ``SynologyDSM`` class can also ``update()`` all APIs at once. print("Memory Use: " + str(api.utilisation.memory_real_usage) + " %") print("Net Up: " + str(api.utilisation.network_up())) print("Net Down: " + str(api.utilisation.network_down())) - + print("--") + print("=== Storage ===") api.storage.update() for volume_id in api.storage.volumes_ids: print("ID: " + str(volume_id)) print("Status: " + str(api.storage.volume_status(volume_id))) print("% Used: " + str(api.storage.volume_percentage_used(volume_id)) + " %") + print("--") for disk_id in api.storage.disks_ids: print("ID: " + str(disk_id)) @@ -113,9 +116,16 @@ The ``SynologyDSM`` class can also ``update()`` all APIs at once. print("S-Status: " + str(api.storage.disk_smart_status(disk_id))) print("Status: " + str(api.storage.disk_status(disk_id))) print("Temp: " + str(api.storage.disk_temp(disk_id))) - - - + print("--") + + print("=== Shared Folders ===") + api.share.update() + for share_uuid in api.share.shares_uuids: + print("Share name: " + str(api.share.share_name(share_uuid))) + print("Share path: " + str(api.share.share_path(share_uuid))) + print("Space used: " + str(api.share.share_size(share_uuid, human_readable=True))) + print("Recycle Bin Enabled: " + str(api.share.share_recycle_bin(share_uuid))) + print("--") Surveillance Station usage -------------------------- @@ -162,6 +172,7 @@ Credits / Special Thanks - https://github.com/chemelli74 (2SA tests) - https://github.com/snjoetw (Surveillance Station library) - https://github.com/shenxn (Surveillance Station tests) +- https://github.com/Gestas (Shared Folders) Found Synology API "documentation" on this repo : https://github.com/kwent/syno/tree/master/definitions diff --git a/synology_dsm/api/core/share.py b/synology_dsm/api/core/share.py new file mode 100644 index 00000000..6e38f40c --- /dev/null +++ b/synology_dsm/api/core/share.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""Shared Folders data.""" +from synology_dsm.helpers import SynoFormatHelper + + +class SynoCoreShare(object): + """Class containing Share data.""" + + API_KEY = "SYNO.Core.Share" + # Syno supports two methods to retrieve resource details, GET and POST. + # GET returns a limited set of keys. With POST the same keys as GET + # are returned plus any keys listed in the "additional" parameter. + # NOTE: The value of the additional key must be a string. + REQUEST_DATA = { + "additional": '["hidden","encryption","is_aclmode","unite_permission","is_support_acl",' + '"is_sync_share","is_force_readonly","force_readonly_reason","recyclebin",' + '"is_share_moving","is_cluster_share","is_exfat_share","is_cold_storage_share",' + '"support_snapshot","share_quota","enable_share_compress","enable_share_cow",' + '"include_cold_storage_share","is_cold_storage_share"]', + "shareType": "all", + } + + def __init__(self, dsm): + self._dsm = dsm + self._data = {} + + def update(self): + """Updates share data.""" + raw_data = self._dsm.post(self.API_KEY, "list", data=self.REQUEST_DATA) + if raw_data: + self._data = raw_data["data"] + + @property + def shares(self): + """Gets all shares.""" + return self._data.get("shares", []) + + @property + def shares_uuids(self): + """Return (internal) share ids.""" + shares = [] + for share in self.shares: + shares.append(share["uuid"]) + return shares + + def get_share(self, share_uuid): + """Returns a specific share by uuid..""" + for share in self.shares: + if share["uuid"] == share_uuid: + return share + return {} + + def share_name(self, share_uuid): + """Return the name of this share.""" + return self.get_share(share_uuid).get("name") + + def share_path(self, share_uuid): + """Return the volume path of this share.""" + return self.get_share(share_uuid).get("vol_path") + + def share_recycle_bin(self, share_uuid): + """Is the recycle bin enabled for this share?""" + return self.get_share(share_uuid).get("enable_recycle_bin") + + def share_size(self, share_uuid, human_readable=False): + """Total size of share.""" + share_size_mb = self.get_share(share_uuid).get("share_quota_used") + # Share size is returned in MB so we convert it. + share_size_bytes = SynoFormatHelper.megabytes_to_bytes(share_size_mb) + if human_readable: + return SynoFormatHelper.bytes_to_readable(share_size_bytes) + return share_size_bytes diff --git a/synology_dsm/api/storage/storage.py b/synology_dsm/api/storage/storage.py index 67dcb1d1..0eb9d3b2 100644 --- a/synology_dsm/api/storage/storage.py +++ b/synology_dsm/api/storage/storage.py @@ -52,7 +52,7 @@ def volumes_ids(self): volumes.append(volume["id"]) return volumes - def _get_volume(self, volume_id): + def get_volume(self, volume_id): """Returns a specific volume.""" for volume in self.volumes: if volume["id"] == volume_id: @@ -61,15 +61,15 @@ def _get_volume(self, volume_id): def volume_status(self, volume_id): """Status of the volume (normal, degraded, etc).""" - return self._get_volume(volume_id).get("status") + return self.get_volume(volume_id).get("status") def volume_device_type(self, volume_id): """Returns the volume type (RAID1, RAID2, etc).""" - return self._get_volume(volume_id).get("device_type") + return self.get_volume(volume_id).get("device_type") def volume_size_total(self, volume_id, human_readable=False): """Total size of volume.""" - volume = self._get_volume(volume_id) + volume = self.get_volume(volume_id) if volume.get("size"): return_data = int(volume["size"]["total"]) if human_readable: @@ -79,7 +79,7 @@ def volume_size_total(self, volume_id, human_readable=False): def volume_size_used(self, volume_id, human_readable=False): """Total used size in volume.""" - volume = self._get_volume(volume_id) + volume = self.get_volume(volume_id) if volume.get("size"): return_data = int(volume["size"]["used"]) if human_readable: @@ -89,7 +89,7 @@ def volume_size_used(self, volume_id, human_readable=False): def volume_percentage_used(self, volume_id): """Total used size in percentage for volume.""" - volume = self._get_volume(volume_id) + volume = self.get_volume(volume_id) if volume.get("size"): total = int(volume["size"]["total"]) used = int(volume["size"]["used"]) @@ -137,7 +137,7 @@ def disks_ids(self): disks.append(disk["id"]) return disks - def _get_disk(self, disk_id): + def get_disk(self, disk_id): """Returns a specific disk.""" for disk in self.disks: if disk["id"] == disk_id: @@ -152,41 +152,41 @@ def _get_disks_for_volume(self, volume_id): if pool.get("deploy_path") == volume_id: # RAID disk redundancy for disk_id in pool["disks"]: - disks.append(self._get_disk(disk_id)) + disks.append(self.get_disk(disk_id)) if pool.get("pool_child"): # SHR disk redundancy for pool_child in pool.get("pool_child"): if pool_child["id"] == volume_id: for disk_id in pool["disks"]: - disks.append(self._get_disk(disk_id)) + disks.append(self.get_disk(disk_id)) return disks def disk_name(self, disk_id): """The name of this disk.""" - return self._get_disk(disk_id).get("name") + return self.get_disk(disk_id).get("name") def disk_device(self, disk_id): """The mount point of this disk.""" - return self._get_disk(disk_id).get("device") + return self.get_disk(disk_id).get("device") def disk_smart_status(self, disk_id): """Status of disk according to S.M.A.R.T).""" - return self._get_disk(disk_id).get("smart_status") + return self.get_disk(disk_id).get("smart_status") def disk_status(self, disk_id): """Status of disk.""" - return self._get_disk(disk_id).get("status") + return self.get_disk(disk_id).get("status") def disk_exceed_bad_sector_thr(self, disk_id): """Checks if disk has exceeded maximum bad sector threshold.""" - return self._get_disk(disk_id).get("exceed_bad_sector_thr") + return self.get_disk(disk_id).get("exceed_bad_sector_thr") def disk_below_remain_life_thr(self, disk_id): """Checks if disk has fallen below minimum life threshold.""" - return self._get_disk(disk_id).get("below_remain_life_thr") + return self.get_disk(disk_id).get("below_remain_life_thr") def disk_temp(self, disk_id): """Returns the temperature of the disk.""" - return self._get_disk(disk_id).get("temp") + return self.get_disk(disk_id).get("temp") diff --git a/synology_dsm/helpers.py b/synology_dsm/helpers.py index b92a967a..e7647f45 100644 --- a/synology_dsm/helpers.py +++ b/synology_dsm/helpers.py @@ -39,3 +39,10 @@ def bytes_to_terrabytes(num): var_tb = num / 1024.0 / 1024.0 / 1024.0 / 1024.0 return round(var_tb, 1) + + @staticmethod + def megabytes_to_bytes(num): + """Converts megabytes to bytes.""" + var_bytes = num * 1024.0 * 1024.0 + + return round(var_bytes, 1) diff --git a/synology_dsm/synology_dsm.py b/synology_dsm/synology_dsm.py index 6d8fe25c..f8da1a50 100644 --- a/synology_dsm/synology_dsm.py +++ b/synology_dsm/synology_dsm.py @@ -18,8 +18,10 @@ SynologyDSMLogin2SARequiredException, SynologyDSMLogin2SAFailedException, ) + from .api.core.security import SynoCoreSecurity from .api.core.utilization import SynoCoreUtilization +from .api.core.share import SynoCoreShare from .api.dsm.information import SynoDSMInformation from .api.dsm.network import SynoDSMNetwork from .api.storage.storage import SynoStorage @@ -73,6 +75,7 @@ def __init__( self._security = None self._utilisation = None self._storage = None + self._share = None self._surveillance = None # Build variables @@ -221,14 +224,29 @@ def _request( params["_sid"] = self._session_id if self._syno_token: params["SynoToken"] = self._syno_token - self._debuglog("Request params: " + str(params)) - # Request data url = self._build_url(api) + + # If the request method is POST and the API is SynoCoreShare the params + # to the request body. Used to support the weird Syno use of POST + # to choose what fields to return. See ./api/core/share.py + # for an example. + if request_method == "POST" and api == SynoCoreShare.API_KEY: + body = {} + body.update(params) + body.update(kwargs.pop("data")) + body["mimeType"] = "application/json" + # Request data via POST (excluding FileStation file uploads) + self._debuglog("POST BODY: " + str(body)) + + kwargs["data"] = body + + # Request data response = self._execute_request(request_method, url, params, **kwargs) + self._debuglog("Request Method: " + request_method) self._debuglog("Successful returned data") self._debuglog("API: " + api) - self._debuglog(str(response)) + self._debuglog("RESPONSE: " + str(response)) # Handle data errors if isinstance(response, dict) and response.get("error") and api != API_AUTH: @@ -305,6 +323,9 @@ def update(self, with_information=False, with_network=False): if self._storage: self._storage.update() + if self._share: + self._share.update() + if self._surveillance: self._surveillance.update() @@ -319,6 +340,9 @@ def reset(self, api): if api == SynoCoreSecurity.API_KEY: self._security = None return True + if api == SynoCoreShare.API_KEY: + self._share = None + return True if api == SynoCoreUtilization.API_KEY: self._utilisation = None return True @@ -331,12 +355,16 @@ def reset(self, api): if isinstance(api, SynoCoreSecurity): self._security = None return True + if isinstance(api, SynoCoreShare): + self._share = None + return True if isinstance(api, SynoCoreUtilization): self._utilisation = None return True if isinstance(api, SynoStorage): self._storage = None return True + if isinstance(api, SynoSurveillanceStation): self._surveillance = None return True @@ -377,6 +405,13 @@ def storage(self): self._storage = SynoStorage(self) return self._storage + @property + def share(self): + """Gets NAS shares information.""" + if not self._share: + self._share = SynoCoreShare(self) + return self._share + @property def surveillance_station(self): """Gets NAS SurveillanceStation.""" diff --git a/tests/__init__.py b/tests/__init__.py index 16c12c0e..700ece00 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,6 +11,7 @@ from synology_dsm.api.dsm.information import SynoDSMInformation from synology_dsm.api.dsm.network import SynoDSMNetwork from synology_dsm.api.storage.storage import SynoStorage +from synology_dsm.api.core.share import SynoCoreShare from synology_dsm.api.surveillance_station import SynoSurveillanceStation from synology_dsm.const import API_AUTH, API_INFO @@ -35,6 +36,7 @@ DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL, DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL, DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, + DSM_6_CORE_SHARE, DSM_6_API_INFO_SURVEILLANCE_STATION, DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM, DSM_6_SURVEILLANCE_STATION_CAMERA_GET_LIVE_VIEW_PATH, @@ -74,6 +76,7 @@ "DSM_NETWORK": DSM_6_DSM_NETWORK, "CORE_SECURITY": DSM_6_CORE_SECURITY, "CORE_UTILIZATION": DSM_6_CORE_UTILIZATION, + "CORE_SHARE": DSM_6_CORE_SHARE, "STORAGE_STORAGE": { "RAID": DSM_6_STORAGE_STORAGE_DS918_PLUS_RAID5_3DISKS_1VOL, "SHR1": DSM_6_STORAGE_STORAGE_DS213_PLUS_SHR1_2DISKS_2VOLS, @@ -132,7 +135,7 @@ def __init__( self.with_surveillance = False def _execute_request(self, method, url, params, **kwargs): - url += urlencode(params) + url += urlencode(params or {}) if "no_internet" in url: raise SynologyDSMRequestException( @@ -196,6 +199,9 @@ def _execute_request(self, method, url, params, **kwargs): if SynoDSMNetwork.API_KEY in url: return API_SWITCHER[self.dsm_version]["DSM_NETWORK"] + if SynoCoreShare.API_KEY in url: + return API_SWITCHER[self.dsm_version]["CORE_SHARE"] + if SynoCoreSecurity.API_KEY in url: if self.error: return DSM_6_CORE_SECURITY_UPDATE_OUTOFDATE diff --git a/tests/api_data/dsm_6/__init__.py b/tests/api_data/dsm_6/__init__.py index d31754f2..88747c05 100644 --- a/tests/api_data/dsm_6/__init__.py +++ b/tests/api_data/dsm_6/__init__.py @@ -21,7 +21,6 @@ DSM_6_STORAGE_STORAGE_DS1515_PLUS_SHR2_10DISKS_1VOL_WITH_EXPANSION, DSM_6_STORAGE_STORAGE_DS1819_PLUS_SHR2_8DISKS_1VOL, ) - from .surveillance_station.const_6_api_info import ( DSM_6_API_INFO as DSM_6_API_INFO_SURVEILLANCE_STATION, ) @@ -31,7 +30,7 @@ DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MOTION_ENUM, DSM_6_SURVEILLANCE_STATION_CAMERA_EVENT_MD_PARAM_SAVE, ) - +from .core.const_6_core_share import DSM_6_CORE_SHARE from .surveillance_station.const_6_surveillance_station_home_mode import ( DSM_6_SURVEILLANCE_STATION_HOME_MODE_GET_INFO, DSM_6_SURVEILLANCE_STATION_HOME_MODE_SWITCH, diff --git a/tests/api_data/dsm_6/core/const_6_core_share.py b/tests/api_data/dsm_6/core/const_6_core_share.py new file mode 100644 index 00000000..08f37662 --- /dev/null +++ b/tests/api_data/dsm_6/core/const_6_core_share.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +"""DSM 6 SYNO.Core.Share data.""" + +DSM_6_CORE_SHARE = { + "data": { + "shares": [ + { + "desc": "Docker Containers", + "enable_recycle_bin": False, + "enable_share_compress": False, + "enable_share_cow": True, + "enc_auto_mount": False, + "encryption": 0, + "force_readonly_reason": "", + "hidden": True, + "is_aclmode": True, + "is_block_snap_action": False, + "is_cluster_share": False, + "is_cold_storage_share": False, + "is_exfat_share": False, + "is_force_readonly": False, + "is_share_moving": False, + "is_support_acl": True, + "is_sync_share": False, + "is_usb_share": False, + "name": "docker", + "quota_value": 0, + "recycle_bin_admin_only": False, + "share_quota_used": 0, + "support_action": 511, + "support_snapshot": True, + "task_id": "", + "unite_permission": False, + "uuid": "78egut02-b5b1-4933-adt8-a9208526d234", + "vol_path": "/volume1", + }, + { + "desc": "", + "enable_recycle_bin": True, + "enable_share_compress": False, + "enable_share_cow": True, + "enc_auto_mount": False, + "encryption": 0, + "force_readonly_reason": "", + "hidden": False, + "is_aclmode": True, + "is_block_snap_action": False, + "is_cluster_share": False, + "is_cold_storage_share": False, + "is_exfat_share": False, + "is_force_readonly": False, + "is_share_moving": False, + "is_support_acl": True, + "is_sync_share": False, + "is_usb_share": False, + "name": "test_share", + "quota_value": 0, + "recycle_bin_admin_only": False, + "share_quota_used": 36146658672640.0, + "support_action": 511, + "support_snapshot": True, + "task_id": "", + "unite_permission": False, + "uuid": "2ee6c06a-8766-48b5-013d-63b18652a393", + "vol_path": "/volume1", + }, + { + "desc": "user home", + "enable_recycle_bin": False, + "enable_share_compress": False, + "enable_share_cow": True, + "enc_auto_mount": False, + "encryption": 0, + "force_readonly_reason": "", + "hidden": False, + "is_aclmode": True, + "is_block_snap_action": False, + "is_cluster_share": False, + "is_cold_storage_share": False, + "is_exfat_share": False, + "is_force_readonly": False, + "is_share_moving": False, + "is_support_acl": True, + "is_sync_share": False, + "is_usb_share": False, + "name": "homes", + "quota_value": 0, + "recycle_bin_admin_only": False, + "share_quota_used": 0.015625, + "support_action": 511, + "support_snapshot": True, + "task_id": "", + "unite_permission": False, + "uuid": "2b829t90-9512-4236-qqe0-d4133e9992d0", + "vol_path": "/volume1", + }, + { + "desc": "Log volume", + "enable_recycle_bin": True, + "enable_share_compress": True, + "enable_share_cow": True, + "enc_auto_mount": True, + "encryption": 0, + "force_readonly_reason": "", + "hidden": True, + "is_aclmode": True, + "is_block_snap_action": False, + "is_cluster_share": False, + "is_cold_storage_share": False, + "is_exfat_share": False, + "is_force_readonly": False, + "is_share_moving": False, + "is_support_acl": True, + "is_sync_share": False, + "is_usb_share": False, + "name": "logs", + "quota_value": 0, + "recycle_bin_admin_only": True, + "share_quota_used": 947.28515625, + "support_action": 511, + "support_snapshot": True, + "task_id": "", + "unite_permission": False, + "uuid": "b9876507-6880-4wes-8d61-6c984c0813ty", + "vol_path": "/volume2", + }, + { + "desc": "VMs", + "enable_recycle_bin": False, + "enable_share_compress": False, + "enable_share_cow": True, + "enc_auto_mount": False, + "encryption": 0, + "force_readonly_reason": "", + "hidden": False, + "is_aclmode": True, + "is_block_snap_action": False, + "is_cluster_share": False, + "is_cold_storage_share": False, + "is_exfat_share": False, + "is_force_readonly": False, + "is_share_moving": False, + "is_support_acl": True, + "is_sync_share": False, + "is_usb_share": False, + "name": "Virtual_Machines", + "quota_value": 0, + "recycle_bin_admin_only": False, + "share_quota_used": 33911668, + "support_action": 511, + "support_snapshot": True, + "task_id": "", + "unite_permission": False, + "uuid": "5416f693-04tt-4re2-b8e4-f6b18731689b", + "vol_path": "/volume3", + }, + ], + "total": 5, + }, + "success": True, +} diff --git a/tests/test_synology_dsm.py b/tests/test_synology_dsm.py index 6b1a5178..360c047d 100644 --- a/tests/test_synology_dsm.py +++ b/tests/test_synology_dsm.py @@ -28,6 +28,8 @@ from .const import SESSION_ID, DEVICE_TOKEN, SYNO_TOKEN # pylint: disable=no-self-use,protected-access + + class TestSynologyDSM(TestCase): """SynologyDSM test cases.""" @@ -716,3 +718,36 @@ def test_surveillance_camera(self): assert self.api.surveillance_station.get_home_mode_status() assert self.api.surveillance_station.set_home_mode(False) assert self.api.surveillance_station.set_home_mode(True) + + def test_shares(self): + """Test shares.""" + assert self.api.share + self.api.share.update() + assert self.api.share.shares + for share_uuid in self.api.share.shares_uuids: + assert self.api.share.share_name(share_uuid) + assert self.api.share.share_path(share_uuid) + assert self.api.share.share_recycle_bin(share_uuid) is not None + assert self.api.share.share_size(share_uuid) is not None + assert self.api.share.share_size(share_uuid, human_readable=True) + + assert ( + self.api.share.share_name("2ee6c06a-8766-48b5-013d-63b18652a393") + == "test_share" + ) + assert ( + self.api.share.share_path("2ee6c06a-8766-48b5-013d-63b18652a393") + == "/volume1" + ) + assert ( + self.api.share.share_recycle_bin("2ee6c06a-8766-48b5-013d-63b18652a393") + is True + ) + assert ( + self.api.share.share_size("2ee6c06a-8766-48b5-013d-63b18652a393") + == 3.790251876432216e19 + ) + assert ( + self.api.share.share_size("2ee6c06a-8766-48b5-013d-63b18652a393", True) + == "32.9Eb" + )