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 14 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 name in api.share.shares_names:
print("Share name: " + str(name))
print("Share path: " + str(api.share.share_path(name)))
print("Space used: " + str(api.share.share_size(name, human_readable=True)))
print("Recycle Bin Enabled: " + str(api.share.share_recycle_bin(name)))
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
77 changes: 77 additions & 0 deletions synology_dsm/api/core/share.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
"""Shared Folders data."""
from synology_dsm.helpers import SynoFormatHelper


class SynoShare(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", [])

def share(self, share_name):
Gestas marked this conversation as resolved.
Show resolved Hide resolved
"""Returns a specific share."""
for share in self.shares:
if share["name"] == share_name:
return share
return {}

@property
def shares_names(self):
Gestas marked this conversation as resolved.
Show resolved Hide resolved
"""Returns (internal) share names."""
shares = []
for share in self.shares:
shares.append(share["name"])
return shares

def share_path(self, name):
"""The volume path of this share."""
return self.share(name).get("vol_path")

def share_recycle_bin(self, name):
"""Is the recycle bin enabled for this share?"""
return self.share(name).get("enable_recycle_bin")

def share_size(self, name, human_readable=False):
"""Total size of share."""
share_size_mb = self.share(name).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

def share_attribute(self, name, attribute):
"""Returns the value of the specified share attribute."""
# Makes it easier to get a specific attribute without requiring a
# function for each.
if attribute in self.share(name):
return self.share(name).get(attribute)
else:
raise ValueError("Specified attribute does not exist: " + attribute)
Gestas marked this conversation as resolved.
Show resolved Hide resolved
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)
43 changes: 39 additions & 4 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 SynoShare
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,30 @@ 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)
response = self._execute_request(request_method, url, params, **kwargs)

# If the request method is POST and the API is SynoShare 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 == SynoShare.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))
response = self._execute_request(
request_method, url, params=None, data=body
)
else:
# 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 @@ -260,6 +279,11 @@ def _execute_request(self, method, url, params, **kwargs):
response = self._session.get(
url, params=encoded_params, timeout=self._timeout, **kwargs
)
elif method == "POST" and "data" in kwargs:
data = kwargs.pop("data")
response = self._session.post(
url, data=data, timeout=self._timeout, **kwargs
)
elif method == "POST":
response = self._session.post(
url, params=params, timeout=self._timeout, **kwargs
Expand Down Expand Up @@ -305,6 +329,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 Down Expand Up @@ -337,6 +364,7 @@ def reset(self, api):
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 = SynoShare(self)
return self._share

@property
def surveillance_station(self):
"""Gets NAS SurveillanceStation."""
Expand Down
2 changes: 1 addition & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,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
20 changes: 20 additions & 0 deletions tests/test_synology_dsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down Expand Up @@ -716,3 +718,21 @@ 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_names
for share_name in self.api.share.shares_names:
Gestas marked this conversation as resolved.
Show resolved Hide resolved
if share_name == "test_share":
continue
assert self.api.share.share_path(share_name)
assert self.api.share.share_recycle_bin(share_name)
assert self.api.share.share_size(share_name)
assert self.api.share.share_size(share_name, True)

assert self.api.share.share_path("test_share") == "/volume1"
assert self.api.share.share_recycle_bin("test_share") == "True"
assert self.api.share.share_size("test_share") == "801622786048.0"
assert self.api.share.share_size("test_share", True) == "746.6Gb"