initial push for version 0.0.2
This commit is contained in:
commit
10a617aea9
8 changed files with 404 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
*.pyc
|
||||||
|
__pycache__
|
||||||
|
flashairup/flashairup.conf
|
||||||
|
flashairup/data/*
|
65
README.md
Normal file
65
README.md
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# flashair uploader
|
||||||
|
|
||||||
|
|
||||||
|
## Installation requirements
|
||||||
|
|
||||||
|
python3, python3-setuptools
|
||||||
|
`sudo apt install python3-setuptools`
|
||||||
|
libcurl4-openssl-dev libssl-dev
|
||||||
|
`sudo apt install libcurl4-openssl-dev libssl-dev`
|
||||||
|
|
||||||
|
|
||||||
|
Move the `flashairup` directory to the desired place (e.g. `/home/<USER>/`), referred now as WORKDIR
|
||||||
|
|
||||||
|
Create the following directory structure inside the WORKDIR:
|
||||||
|
|
||||||
|
```
|
||||||
|
├── flashairup
|
||||||
|
│ ├── data
|
||||||
|
│ │ ├── cam1
|
||||||
|
│ │ │ ├── last.txt
|
||||||
|
│ │ │ └── pictures
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── flashairup.conf
|
||||||
|
│ └── ...
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
where `cam1` is only an example for the name of a cam.
|
||||||
|
The `last.txt` is needed for keeping track of the index of the last synchronised pictures.
|
||||||
|
The `flashairup.conf` should at least contain the `DEFAULT` section:
|
||||||
|
|
||||||
|
```
|
||||||
|
[DEFAULT]
|
||||||
|
ftp_srv = FTP-SERVER
|
||||||
|
ftp_path = PATH
|
||||||
|
ftp_user = USER
|
||||||
|
ftp_pwd = PASSWORD
|
||||||
|
db = new
|
||||||
|
# Set the logger level (DEBUG, INFO, WARNING, ERROR)
|
||||||
|
log_level = DEBUG
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a cam section to the config file:
|
||||||
|
|
||||||
|
```
|
||||||
|
[cam1] # cam name
|
||||||
|
ip = CAM-IP
|
||||||
|
location = LOCATION-OF-THE-PICTURES-ON-CAM
|
||||||
|
```
|
||||||
|
|
||||||
|
Example config:
|
||||||
|
|
||||||
|
```
|
||||||
|
[DEFAULT]
|
||||||
|
ftp_srv = my.ftp.server.org
|
||||||
|
ftp_path = media/pictures
|
||||||
|
ftp_user = ftpuser
|
||||||
|
ftp_pwd = strongpassword1
|
||||||
|
db = new
|
||||||
|
# Set the logger level (DEBUG, INFO, WARNING, ERROR)
|
||||||
|
log_level = DEBUG
|
||||||
|
|
||||||
|
[cam1]
|
||||||
|
ip = 10.0.0.10
|
||||||
|
location = DCIM/101NIKON
|
||||||
|
```
|
0
flashairup/__init__.py
Normal file
0
flashairup/__init__.py
Normal file
216
flashairup/cam.py
Normal file
216
flashairup/cam.py
Normal file
|
@ -0,0 +1,216 @@
|
||||||
|
import pycurl
|
||||||
|
import os
|
||||||
|
from io import BytesIO
|
||||||
|
import subprocess
|
||||||
|
from ftplib import FTP
|
||||||
|
import signal
|
||||||
|
|
||||||
|
class Cam(object):
|
||||||
|
""" Cam object
|
||||||
|
|
||||||
|
Attributes
|
||||||
|
----------
|
||||||
|
log : logging_object
|
||||||
|
name : str
|
||||||
|
name of the cam, set by the section in the config file
|
||||||
|
ip : str
|
||||||
|
IP of the cam in string format
|
||||||
|
location : str
|
||||||
|
Location of the picutres on the SD-Card
|
||||||
|
ftp_config : config_object
|
||||||
|
FTP configuration; such as URI, username and password
|
||||||
|
|
||||||
|
|
||||||
|
Methods
|
||||||
|
-------
|
||||||
|
getFileList()
|
||||||
|
Get the file list of the cam via curl
|
||||||
|
organiseMissing(files)
|
||||||
|
Get a list of files and decide which to download
|
||||||
|
getImage(filename)
|
||||||
|
Get Images from cam via curl
|
||||||
|
uploadImage(filename)
|
||||||
|
Upload the image to remote FTP server
|
||||||
|
isOnline()
|
||||||
|
Check if the cam is online via ping
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, log, name, ip, location, ftp_config):
|
||||||
|
self.log = log
|
||||||
|
self.name = name
|
||||||
|
self.ip = ip
|
||||||
|
self.getFileListPath = 'http://' + self.ip + '/command.cgi?op=100&DIR=' + location
|
||||||
|
self.getFilePath = 'http://' + self.ip + '/' + location
|
||||||
|
self.ftp_config = ftp_config
|
||||||
|
|
||||||
|
#TODO: Test what happens if file is not present
|
||||||
|
fname = './data/' + self.name + '/last.txt'
|
||||||
|
self.last = str(0)
|
||||||
|
try:
|
||||||
|
with open(fname) as last_file:
|
||||||
|
for line in last_file:
|
||||||
|
self.last = line.strip().split()[0]
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.log.debug(self.name + " last.txt was not found, default value 0 is set")
|
||||||
|
|
||||||
|
self.log.debug(self.name + " last index is " + self.last)
|
||||||
|
|
||||||
|
|
||||||
|
def getFileList(self):
|
||||||
|
"""Get the file list of the cam via curl
|
||||||
|
|
||||||
|
Return: list of filenames
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Assert that the cam is online, otherwise the whole function doesn't make sense
|
||||||
|
assert self.isOnline()
|
||||||
|
|
||||||
|
self.log.info(self.name + " get list of files")
|
||||||
|
|
||||||
|
# Open, run and close the curl session
|
||||||
|
crl = pycurl.Curl()
|
||||||
|
b_obj = BytesIO()
|
||||||
|
crl.setopt(pycurl.CONNECTTIMEOUT, 5)
|
||||||
|
crl.setopt(pycurl.TIMEOUT, 5)
|
||||||
|
crl.setopt(crl.URL, self.getFileListPath)
|
||||||
|
crl.setopt(crl.WRITEDATA, b_obj)
|
||||||
|
crl.perform()
|
||||||
|
crl.close()
|
||||||
|
|
||||||
|
# Get the file list
|
||||||
|
get_list = b_obj.getvalue().decode('utf8').splitlines()
|
||||||
|
del get_list[0]
|
||||||
|
|
||||||
|
# Create empty list for processing later
|
||||||
|
files = []
|
||||||
|
|
||||||
|
# Get the filenames only
|
||||||
|
for line in get_list:
|
||||||
|
files.append(line.split(',')[1])
|
||||||
|
|
||||||
|
self.log.debug(self.name + " " + " ".join(files))
|
||||||
|
|
||||||
|
# Return the file list
|
||||||
|
return files
|
||||||
|
|
||||||
|
def organiseMissing(self, files):
|
||||||
|
"""Get a list of files and decide which to download
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
files: list
|
||||||
|
List of files
|
||||||
|
"""
|
||||||
|
# Go trough the filenames, assuming they are sorted
|
||||||
|
#TODO: If possible try to make it work without the assumption
|
||||||
|
# Do this with another index (current highest) and update this index only with a bigger number
|
||||||
|
# In the last step, update self.last with current highest index (and obviously update the value in the file
|
||||||
|
for filename in files:
|
||||||
|
file_index = int(''.join(list(filter(str.isdigit, filename))))
|
||||||
|
|
||||||
|
# Check if the file needs to be downloaded
|
||||||
|
if file_index > int(self.last):
|
||||||
|
self.log.info("Found a file: "+ filename)
|
||||||
|
# Download the file
|
||||||
|
self.getImage(filename)
|
||||||
|
self.log.info("Image " + filename + " downloaded")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Upload images to corresponding FTP server
|
||||||
|
self.uploadImage(filename)
|
||||||
|
self.log.info("File " + filename + " uploaded to remote server")
|
||||||
|
# Remove the local file
|
||||||
|
os.remove('./data/' + self.name + '/pictures/' + filename)
|
||||||
|
self.log.info("File " + filename + " removed from local storage")
|
||||||
|
# Update the last index
|
||||||
|
self.last = file_index
|
||||||
|
except ConnectionRefusedError:
|
||||||
|
self.log.error("Could not reach FTP server, exit now")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Update the last file for each cam only once
|
||||||
|
last_file = open('./data/' + self.name + '/last.txt', "w+")
|
||||||
|
last_file.write(str(self.last))
|
||||||
|
last_file.close()
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def getImage(self, filename):
|
||||||
|
"""Get Images from cam via curl
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename : str
|
||||||
|
Filnema of the image to download
|
||||||
|
"""
|
||||||
|
|
||||||
|
assert self.isOnline()
|
||||||
|
|
||||||
|
# Initialize the curl object, set timeout to 5 seconds, otherwise curl might hang if
|
||||||
|
# the cam is suddenly not reachable anymore
|
||||||
|
crl = pycurl.Curl()
|
||||||
|
crl.setopt(pycurl.CONNECTTIMEOUT, 5)
|
||||||
|
crl.setopt(pycurl.TIMEOUT, 5)
|
||||||
|
self.log.info("Download Picture from " + self.getFilePath + '/' + filename)
|
||||||
|
|
||||||
|
#TODO: Set data path in default config section
|
||||||
|
with open('./data/'+ self.name +'/pictures/' + filename, 'wb') as f:
|
||||||
|
signal.signal(signal.SIGALRM, self.handler)
|
||||||
|
signal.alarm(5)
|
||||||
|
crl.setopt(crl.URL, self.getFilePath + '/' + filename)
|
||||||
|
crl.setopt(crl.WRITEDATA, f)
|
||||||
|
crl.perform()
|
||||||
|
crl.close()
|
||||||
|
signal.alarm(0)
|
||||||
|
|
||||||
|
|
||||||
|
def uploadImage(self, filename):
|
||||||
|
"""Upload the image to remote FTP server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
filename : str
|
||||||
|
Filename for the file which should be uploaded
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Only if the config is set to new use the additional prefix, otherwise none
|
||||||
|
if self.ftp_config['db'] == "new":
|
||||||
|
prefix = "_neue_db"
|
||||||
|
else:
|
||||||
|
prefix = ""
|
||||||
|
|
||||||
|
# Initialize FTP settings
|
||||||
|
ftp_path = self.ftp_config['ftp_path'] + '/' + self.name + prefix
|
||||||
|
#TODO: Move to config file
|
||||||
|
ftp = FTP("xray876.server4you.net")
|
||||||
|
ftp.login(self.ftp_config['ftp_user'], self.ftp_config['ftp_pwd'])
|
||||||
|
ftp.cwd(ftp_path)
|
||||||
|
|
||||||
|
with open('data/' + self.name + '/pictures/' + filename, 'rb') as f:
|
||||||
|
ftp.storbinary('STOR %s' % os.path.basename(filename), f)
|
||||||
|
|
||||||
|
ftp.quit()
|
||||||
|
|
||||||
|
|
||||||
|
def isOnline(self):
|
||||||
|
"""Check if the Cam is online via ping
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
0
|
||||||
|
cam is online
|
||||||
|
1
|
||||||
|
cam is offline
|
||||||
|
"""
|
||||||
|
command = ['ping', '-c', '1', '-w', '1', '-q', self.ip]
|
||||||
|
|
||||||
|
return subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode == 0
|
||||||
|
|
||||||
|
def handler(self, signum, frame):
|
||||||
|
self.log.debug("Signal handler called with signal" + signum)
|
||||||
|
raise OSError("Could not reach camera")
|
||||||
|
|
||||||
|
|
33
flashairup/config.py
Normal file
33
flashairup/config.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import configparser
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
""" Config Class, parses the arguments and the config from the flashairup.conf ini file"""
|
||||||
|
def __init__(self, arguments):
|
||||||
|
""" read arguments dicts as a base """
|
||||||
|
|
||||||
|
self.arguments = arguments
|
||||||
|
|
||||||
|
# Read settings from flashairup.conf ini configfile
|
||||||
|
self.configfile = "flashairup.conf"
|
||||||
|
self.conf = configparser.ConfigParser()
|
||||||
|
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
""" loads the config file"""
|
||||||
|
self.conf.read(self.configfile)
|
||||||
|
|
||||||
|
|
||||||
|
# def write(self):
|
||||||
|
# print("writing the following config to file..\n")
|
||||||
|
# self.print()
|
||||||
|
# with open('flashairup.conf', 'w') as configf:
|
||||||
|
# self.conf.write(configf)
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# def print(self):
|
||||||
|
# for section in ['DEFAULT'] + self.conf.sections() :
|
||||||
|
# print('[' + section + ']')
|
||||||
|
# for key in self.conf[section]:
|
||||||
|
# print(key + ' = ' + self.conf[section][key])
|
||||||
|
# print('\n')
|
82
flashairup/main.py
Executable file
82
flashairup/main.py
Executable file
|
@ -0,0 +1,82 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import logging.handlers
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from cam import Cam
|
||||||
|
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
arg_parser = argparse.ArgumentParser(description='flashair uploader')
|
||||||
|
# check if cams are online, if yes download all new pictures and upload them to the remote server
|
||||||
|
arg_parser.add_argument('--sync', help="synchronise pictures from the cams to the ftp server", action='store_true')
|
||||||
|
|
||||||
|
arguments = vars(arg_parser.parse_args())
|
||||||
|
|
||||||
|
|
||||||
|
### logger
|
||||||
|
FORMAT = '%(asctime)-15s %(message)s'
|
||||||
|
logging.basicConfig(format=FORMAT)
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
# Also log output to syslog
|
||||||
|
handler = logging.handlers.SysLogHandler(address = '/dev/log')
|
||||||
|
fmt = 'flashair-uploader[%(process)-5s:%(thread)d]: ' \
|
||||||
|
'%(levelname)-5s %(message)s'
|
||||||
|
handler.setFormatter(logging.Formatter(fmt=fmt))
|
||||||
|
log.addHandler(handler)
|
||||||
|
|
||||||
|
### Main function
|
||||||
|
def main(arguments):
|
||||||
|
"""
|
||||||
|
Gets the parsed arguments and loads the configurations.
|
||||||
|
If the arguments was "--sync" the sync procedure starts
|
||||||
|
|
||||||
|
Input: parsed arguments
|
||||||
|
Output: NULL
|
||||||
|
"""
|
||||||
|
# load the configuration
|
||||||
|
log.debug("loading configuration")
|
||||||
|
config = Config(arguments)
|
||||||
|
config.load()
|
||||||
|
log.info("configuration loaded")
|
||||||
|
|
||||||
|
# Set the defined log level
|
||||||
|
try:
|
||||||
|
log.setLevel(config.conf['DEFAULT']['log_level'])
|
||||||
|
except ValueError:
|
||||||
|
log.error("The configured LogLevel is wrong " + config.conf['DEFAULT']['log_level'])
|
||||||
|
|
||||||
|
log.debug("LogLevel: " + config.conf['DEFAULT']['log_level'])
|
||||||
|
|
||||||
|
# Do a sync if correct arguments are passed
|
||||||
|
if arguments['sync']:
|
||||||
|
log.debug("syncing pictures from cams")
|
||||||
|
sync(config)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def sync(config):
|
||||||
|
"""
|
||||||
|
Starts the sync procedure for all cameras if and only if they are online
|
||||||
|
|
||||||
|
Input: config object
|
||||||
|
Output: NULL
|
||||||
|
"""
|
||||||
|
# Loads all cams
|
||||||
|
cams = config.conf.sections()
|
||||||
|
# Iterate through the cams
|
||||||
|
for cam_i in cams:
|
||||||
|
log.debug("initialize cam object for " + cam_i)
|
||||||
|
# Creates a Cam object for each cam
|
||||||
|
cam = Cam(log, cam_i, config.conf[cam_i]['ip'], config.conf[cam_i]['location'], config.conf.defaults())
|
||||||
|
try:
|
||||||
|
files = cam.getFileList()
|
||||||
|
cam.organiseMissing(files)
|
||||||
|
except AssertionError:
|
||||||
|
log.error(cam_i + " at IP: " + cam.ip + " is offline")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Start with the main function
|
||||||
|
main(arguments)
|
1
flashairup/version.py
Normal file
1
flashairup/version.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
VERSION = "0.0.2"
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
config==0.5.0.post0
|
||||||
|
configparser==5.0.0
|
||||||
|
pycurl==7.43.0.5
|
Loading…
Add table
Reference in a new issue