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

Persistent Storage #1131

Merged
merged 12 commits into from
Aug 23, 2024
2 changes: 1 addition & 1 deletion Dockerfile.el9
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ RUN rpmspec -q --buildrequires /python-apprise.spec | cut -f1 -d' ' | \
xargs dnf install -y

# RPM Build Structure Setup
ENV FLAVOR=rpmbuild OS=centos DIST=el8
ENV FLAVOR=rpmbuild OS=centos DIST=el9
RUN useradd builder -u 1000 -m -G users,wheel &>/dev/null && \
echo "builder ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers

Expand Down
3 changes: 3 additions & 0 deletions apprise/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
from .common import CONTENT_INCLUDE_MODES
from .common import ContentLocation
from .common import CONTENT_LOCATIONS
from .common import PersistentStoreMode
from .common import PERSISTENT_STORE_MODES

from .url import URLBase
from .url import PrivacyMode
Expand Down Expand Up @@ -84,6 +86,7 @@
'ConfigFormat', 'CONFIG_FORMATS',
'ContentIncludeMode', 'CONTENT_INCLUDE_MODES',
'ContentLocation', 'CONTENT_LOCATIONS',
'PersistentStoreMode', 'PERSISTENT_STORE_MODES',
'PrivacyMode',

# Managers
Expand Down
99 changes: 98 additions & 1 deletion apprise/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from os.path import isfile
from os.path import abspath
from .common import NotifyType
from .common import PersistentStoreMode
from .manager_plugins import NotificationManager


Expand Down Expand Up @@ -157,6 +158,22 @@
# By default, no paths are scanned.
__plugin_paths = []

# Optionally set the location of the persistent storage
# By default there is no path and thus persistent storage is not used
__storage_path = None

# Optionally define the default salt to apply to all persistent storage
# namespace generation (unless over-ridden)
__storage_salt = b''

# Optionally define the namespace length of the directories created by
# the storage. If this is set to zero, then the length is pre-determined
# by the generator (sha1, md5, sha256, etc)
__storage_idlen = 8

# Set storage to auto
__storage_mode = PersistentStoreMode.AUTO

# All internal/system flags are prefixed with an underscore (_)
# These can only be initialized using Python libraries and are not picked
# up from (yaml) configuration files (if set)
Expand All @@ -171,7 +188,9 @@
# A unique identifer we can use to associate our calling source
_uid = str(uuid4())

def __init__(self, plugin_paths=None, **kwargs):
def __init__(self, plugin_paths=None, storage_path=None,
storage_mode=None, storage_salt=None,
storage_idlen=None, **kwargs):
"""
Asset Initialization

Expand All @@ -187,8 +206,49 @@

if plugin_paths:
# Load any decorated modules if defined
self.__plugin_paths = plugin_paths
N_MGR.module_detection(plugin_paths)

if storage_path:
# Define our persistent storage path
self.__storage_path = storage_path

if storage_mode:
# Define how our persistent storage behaves
self.__storage_mode = storage_mode

if isinstance(storage_idlen, int):
# Define the number of characters utilized from our namespace lengh
if storage_idlen < 0:
# Unsupported type
raise ValueError(

Check warning on line 224 in apprise/asset.py

View check run for this annotation

Codecov / codecov/patch

apprise/asset.py#L224

Added line #L224 was not covered by tests
'AppriseAsset storage_idlen(): Value must '
'be an integer and > 0')

# Store value
self.__storage_idlen = storage_idlen

Check warning on line 229 in apprise/asset.py

View check run for this annotation

Codecov / codecov/patch

apprise/asset.py#L229

Added line #L229 was not covered by tests

if storage_salt is not None:
# Define the number of characters utilized from our namespace lengh

if isinstance(storage_salt, bytes):
self.__storage_salt = storage_salt

elif isinstance(storage_salt, str):
try:
self.__storage_salt = storage_salt.encode(self.encoding)

except UnicodeEncodeError:
# Bad data; don't pass it along
raise ValueError(
'AppriseAsset namespace_salt(): '
'Value provided could not be encoded')

else: # Unsupported
raise ValueError(

Check warning on line 248 in apprise/asset.py

View check run for this annotation

Codecov / codecov/patch

apprise/asset.py#L248

Added line #L248 was not covered by tests
'AppriseAsset namespace_salt(): Value provided must be '
'string or bytes object')

def color(self, notify_type, color_type=None):
"""
Returns an HTML mapped color based on passed in notify type
Expand Down Expand Up @@ -356,3 +416,40 @@

"""
return int(value.lstrip('#'), 16)

@property
def plugin_paths(self):
"""
Return the plugin paths defined
"""
return self.__plugin_paths

@property
def storage_path(self):
"""
Return the persistent storage path defined
"""
return self.__storage_path

@property
def storage_mode(self):
"""
Return the persistent storage mode defined
"""

return self.__storage_mode

@property
def storage_salt(self):
"""
Return the provided namespace salt; this is always of type bytes
"""
return self.__storage_salt

@property
def storage_idlen(self):
"""
Return the persistent storage id length
"""

return self.__storage_idlen
8 changes: 6 additions & 2 deletions apprise/attachment/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import re
import os
from .base import AttachBase
from ..utils import path_decode
from ..common import ContentLocation
from ..locale import gettext_lazy as _

Expand Down Expand Up @@ -57,7 +58,10 @@ def __init__(self, path, **kwargs):

# Store path but mark it dirty since we have not performed any
# verification at this point.
self.dirty_path = os.path.expanduser(path)
self.dirty_path = path_decode(path)

# Track our file as it was saved
self.__original_path = os.path.normpath(path)
return

def url(self, privacy=False, *args, **kwargs):
Expand All @@ -77,7 +81,7 @@ def url(self, privacy=False, *args, **kwargs):
params['name'] = self._name

return 'file://{path}{params}'.format(
path=self.quote(self.dirty_path),
path=self.quote(self.__original_path),
params='?{}'.format(self.urlencode(params, safe='/'))
if params else '',
)
Expand Down
111 changes: 72 additions & 39 deletions apprise/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,18 +35,15 @@

from os.path import isfile
from os.path import exists
from os.path import expanduser
from os.path import expandvars

from . import NotifyType
from . import NotifyFormat
from . import Apprise
from . import AppriseAsset
from . import AppriseConfig

from .utils import parse_list
from .utils import parse_list, path_decode
from .common import NOTIFY_TYPES
from .common import NOTIFY_FORMATS
from .common import PERSISTENT_STORE_MODES
from .common import ContentLocation
from .logger import logger

Expand Down Expand Up @@ -104,59 +101,69 @@
'/var/lib/apprise/plugins',
)

#
# Persistent Storage
#
DEFAULT_STORAGE_PATH = '~/.local/share/apprise/cache'

# Detect Windows
if platform.system() == 'Windows':
# Default Config Search Path for Windows Users
DEFAULT_CONFIG_PATHS = (
expandvars('%APPDATA%\\Apprise\\apprise'),
expandvars('%APPDATA%\\Apprise\\apprise.conf'),
expandvars('%APPDATA%\\Apprise\\apprise.yml'),
expandvars('%APPDATA%\\Apprise\\apprise.yaml'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.conf'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yml'),
expandvars('%LOCALAPPDATA%\\Apprise\\apprise.yaml'),
'%APPDATA%\\Apprise\\apprise',
'%APPDATA%\\Apprise\\apprise.conf',
'%APPDATA%\\Apprise\\apprise.yml',
'%APPDATA%\\Apprise\\apprise.yaml',
'%LOCALAPPDATA%\\Apprise\\apprise',
'%LOCALAPPDATA%\\Apprise\\apprise.conf',
'%LOCALAPPDATA%\\Apprise\\apprise.yml',
'%LOCALAPPDATA%\\Apprise\\apprise.yaml',

#
# Global Support
#

# C:\ProgramData\Apprise
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.conf'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yml'),
expandvars('%ALLUSERSPROFILE%\\Apprise\\apprise.yaml'),
'%ALLUSERSPROFILE%\\Apprise\\apprise',
'%ALLUSERSPROFILE%\\Apprise\\apprise.conf',
'%ALLUSERSPROFILE%\\Apprise\\apprise.yml',
'%ALLUSERSPROFILE%\\Apprise\\apprise.yaml',

# C:\Program Files\Apprise
expandvars('%PROGRAMFILES%\\Apprise\\apprise'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.conf'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yml'),
expandvars('%PROGRAMFILES%\\Apprise\\apprise.yaml'),
'%PROGRAMFILES%\\Apprise\\apprise',
'%PROGRAMFILES%\\Apprise\\apprise.conf',
'%PROGRAMFILES%\\Apprise\\apprise.yml',
'%PROGRAMFILES%\\Apprise\\apprise.yaml',

# C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.conf'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yml'),
expandvars('%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml'),
'%COMMONPROGRAMFILES%\\Apprise\\apprise',
'%COMMONPROGRAMFILES%\\Apprise\\apprise.conf',
'%COMMONPROGRAMFILES%\\Apprise\\apprise.yml',
'%COMMONPROGRAMFILES%\\Apprise\\apprise.yaml',
)

# Default Plugin Search Path for Windows Users
DEFAULT_PLUGIN_PATHS = (
expandvars('%APPDATA%\\Apprise\\plugins'),
expandvars('%LOCALAPPDATA%\\Apprise\\plugins'),
'%APPDATA%\\Apprise\\plugins',
'%LOCALAPPDATA%\\Apprise\\plugins',

#
# Global Support
#

# C:\ProgramData\Apprise\plugins
expandvars('%ALLUSERSPROFILE%\\Apprise\\plugins'),
'%ALLUSERSPROFILE%\\Apprise\\plugins',
# C:\Program Files\Apprise\plugins
expandvars('%PROGRAMFILES%\\Apprise\\plugins'),
'%PROGRAMFILES%\\Apprise\\plugins',
# C:\Program Files\Common Files
expandvars('%COMMONPROGRAMFILES%\\Apprise\\plugins'),
'%COMMONPROGRAMFILES%\\Apprise\\plugins',
)

#
# Persistent Storage
#
DEFAULT_STORAGE_PATH = '%APPDATA%/Apprise/cache'


def print_help_msg(command):
"""
Expand Down Expand Up @@ -190,23 +197,34 @@ def print_version_msg():
@click.option('--plugin-path', '-P', default=None, type=str, multiple=True,
metavar='PLUGIN_PATH',
help='Specify one or more plugin paths to scan.')
@click.option('--storage-path', '-S', default=DEFAULT_STORAGE_PATH, type=str,
metavar='STORAGE_PATH',
help='Specify the path to the persistent storage location '
'(default={}).'.format(DEFAULT_STORAGE_PATH))
@click.option('--storage-mode', '-SM', default=PERSISTENT_STORE_MODES[0],
type=str, metavar='MODE',
help='Persistent disk storage write mode (default={}). '
'Possible values are "{}", and "{}".'.format(
PERSISTENT_STORE_MODES[0], '", "'.join(
PERSISTENT_STORE_MODES[:-1]),
PERSISTENT_STORE_MODES[-1]))
@click.option('--config', '-c', default=None, type=str, multiple=True,
metavar='CONFIG_URL',
help='Specify one or more configuration locations.')
@click.option('--attach', '-a', default=None, type=str, multiple=True,
metavar='ATTACHMENT_URL',
help='Specify one or more attachment.')
@click.option('--notification-type', '-n', default=NotifyType.INFO, type=str,
@click.option('--notification-type', '-n', default=NOTIFY_TYPES[0], type=str,
metavar='TYPE',
help='Specify the message type (default={}). '
'Possible values are "{}", and "{}".'.format(
NotifyType.INFO, '", "'.join(NOTIFY_TYPES[:-1]),
NOTIFY_TYPES[0], '", "'.join(NOTIFY_TYPES[:-1]),
NOTIFY_TYPES[-1]))
@click.option('--input-format', '-i', default=NotifyFormat.TEXT, type=str,
@click.option('--input-format', '-i', default=NOTIFY_FORMATS[0], type=str,
metavar='FORMAT',
help='Specify the message input format (default={}). '
'Possible values are "{}", and "{}".'.format(
NotifyFormat.TEXT, '", "'.join(NOTIFY_FORMATS[:-1]),
NOTIFY_FORMATS[0], '", "'.join(NOTIFY_FORMATS[:-1]),
NOTIFY_FORMATS[-1]))
@click.option('--theme', '-T', default='default', type=str, metavar='THEME',
help='Specify the default theme.')
Expand Down Expand Up @@ -243,8 +261,8 @@ def print_version_msg():
metavar='SERVER_URL [SERVER_URL2 [SERVER_URL3]]',)
def main(body, title, config, attach, urls, notification_type, theme, tag,
input_format, dry_run, recursion_depth, verbose, disable_async,
details, interpret_escapes, interpret_emojis, plugin_path, debug,
version):
details, interpret_escapes, interpret_emojis, plugin_path,
storage_path, storage_mode, debug, version):
"""
Send a notification to all of the specified servers identified by their
URLs the content provided within the title, body and notification-type.
Expand Down Expand Up @@ -318,11 +336,20 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
# issue. For consistency, we also return a 2
sys.exit(2)

storage_mode = storage_mode.strip().lower()
if storage_mode not in PERSISTENT_STORE_MODES:
logger.error(
'The --storage-mode (-SM) value of {} is not supported.'
.format(storage_mode))
# 2 is the same exit code returned by Click if there is a parameter
# issue. For consistency, we also return a 2
sys.exit(2)

if not plugin_path:
# Prepare a default set of plugin path
plugin_path = \
next((path for path in DEFAULT_PLUGIN_PATHS
if exists(expanduser(path))), None)
[path for path in DEFAULT_PLUGIN_PATHS
if exists(path_decode(path))]

# Prepare our asset
asset = AppriseAsset(
Expand All @@ -346,6 +373,12 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,

# Load our plugins
plugin_paths=plugin_path,

# Load our persistent storage path
storage_path=storage_path,

# Define if we flush to disk as soon as possible or not when required
storage_mode=storage_mode
)

# Create our Apprise object
Expand Down Expand Up @@ -483,7 +516,7 @@ def main(body, title, config, attach, urls, notification_type, theme, tag,
else:
# Load default configuration
a.add(AppriseConfig(
paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(expanduser(f))],
paths=[f for f in DEFAULT_CONFIG_PATHS if isfile(path_decode(f))],
asset=asset, recursion=recursion_depth))

if len(a) == 0 and not urls:
Expand Down
Loading
Loading