Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Shared Folders support #61

Merged
merged 22 commits into from
Jul 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,30 +92,40 @@ 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()
print("CPU Load: " + str(api.utilisation.cpu_total_load) + " %")
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("--")
Gestas marked this conversation as resolved.
Show resolved Hide resolved

for disk_id in api.storage.disks_ids:
print("ID: " + str(disk_id))
print("Name: " + str(api.storage.disk_name(disk_id)))
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
--------------------------

Expand Down Expand Up @@ -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

Expand Down
72 changes: 72 additions & 0 deletions synology_dsm/api/core/share.py
Original file line number Diff line number Diff line change
@@ -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
32 changes: 16 additions & 16 deletions synology_dsm/api/storage/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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"])
Expand Down Expand Up @@ -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:
Expand All @@ -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")
7 changes: 7 additions & 0 deletions synology_dsm/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
41 changes: 38 additions & 3 deletions synology_dsm/synology_dsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -73,6 +75,7 @@ def __init__(
self._security = None
self._utilisation = None
self._storage = None
self._share = None
self._surveillance = None

# Build variables
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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()

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Expand Down
8 changes: 7 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
Loading