Skip to content

Commit fd6b250

Browse files
committed
generation pkg
1 parent c22acbe commit fd6b250

5 files changed

Lines changed: 306 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*.*.*'
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v2
13+
- name: copy sources
14+
run: |
15+
mkdir -p .debpkg/etc/watch_and_copy
16+
cp config.json.exemple .debpkg/etc
17+
mkdir -p .debpkg/usr/bin
18+
cp watch_and_copy.py .debpkg/usr/bin
19+
chmod 700 .debpkg/usr/bin/watch_and_copy.py
20+
cp README.* .debpkg/etc/watch_and_copy
21+
mkdir -p .debpkg/lib/systemd/system/
22+
cp watch_and_copy.service .debpkg/lib/systemd/system
23+
mkdir -p .debpkg/DEBIAN
24+
cp DEBIAN/postinst .debpkg/DEBIAN
25+
chmod 755 .debpkg/DEBIAN/postinst
26+
- uses: jiro4989/build-deb-action@v3
27+
with:
28+
package: watch-and-copy
29+
package_root: .debpkg
30+
maintainer: Libertech
31+
version: ${{ github.ref }} # refs/tags/v*.*.*
32+
arch: 'amd64'
33+
depends: 'python3'
34+
desc: 'watch and copy'
35+
homepage: 'https://github.com/Libertech-FR/watch_and_copy'
36+
- uses: svenstaro/upload-release-action@v2
37+
with:
38+
repo_token: ${{ secrets.GITHUB_TOKEN }}
39+
file: watch-and-copy*
40+
overwrite: true
41+
file_glob: true

DEBIAN/postinst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#!/bin/sh
2+
set -e
3+
SERVICE="watch_and_copy.service"
4+
if command -v systemctl >/dev/null 2>&1; then
5+
systemctl daemon-reload
6+
# Active le service au démarrage
7+
systemctl enable "$SERVICE"
8+
9+
# S'il tourne déjà => restart
10+
# Sinon => start
11+
if systemctl is-active --quiet "$SERVICE"; then
12+
systemctl restart "$SERVICE"
13+
else
14+
if [ -f /etc/watch_and_copy/config.json ]; then
15+
systemctl start "$SERVICE"
16+
fi
17+
fi
18+
19+
exit 0

config.json.exemple

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"source_dir": "/home/source",
3+
"dest_dir": "/mnt/destination"
4+
}

watch_and_copy.py

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Surveille un répertoire source (récursivement) et copie tout nouveau fichier
4+
créé dans un répertoire destination (à plat, sans préserver la structure).
5+
6+
Les répertoires source/destination peuvent être fournis en ligne de commande,
7+
sinon ils sont lus dans config.json (à côté de ce script).
8+
9+
Usage:
10+
python watch_and_copy.py run [<source_dir> <dest_dir>] # foreground
11+
python watch_and_copy.py start [<source_dir> <dest_dir>] # daemon
12+
python watch_and_copy.py stop
13+
python watch_and_copy.py status
14+
python watch_and_copy.py restart [<source_dir> <dest_dir>] # daemon
15+
"""
16+
17+
import os
18+
import sys
19+
import json
20+
import shutil
21+
import signal
22+
import logging
23+
from pathlib import Path
24+
from watchdog.observers import Observer
25+
from watchdog.events import FileSystemEventHandler
26+
27+
PID_FILE = Path("/tmp/watch_and_copy.pid")
28+
LOG_FILE = Path("/tmp/watch_and_copy.log")
29+
CONFIG_FILE = Path("/etc/watch_and_copy/config.json")
30+
31+
32+
def load_config() -> dict:
33+
if not CONFIG_FILE.exists():
34+
return {}
35+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
36+
return json.load(f)
37+
38+
39+
def setup_logging(foreground: bool = False):
40+
logging.basicConfig(
41+
filename=None if foreground else LOG_FILE,
42+
level=logging.INFO,
43+
format="%(asctime)s %(message)s",
44+
datefmt="%Y-%m-%d %H:%M:%S",
45+
)
46+
47+
48+
class CopyOnCreate(FileSystemEventHandler):
49+
def __init__(self, dest: Path):
50+
self.dest = dest
51+
52+
def on_created(self, event):
53+
if event.is_directory:
54+
return
55+
src = Path(event.src_path if isinstance(event.src_path, str) else event.src_path.decode())
56+
dst = self.dest / src.name
57+
shutil.copy2(src, dst)
58+
logging.info(f"Copié {src}{dst}")
59+
60+
61+
def daemonize():
62+
# Double fork pour détacher du terminal
63+
if os.fork() > 0:
64+
sys.exit(0)
65+
os.setsid()
66+
if os.fork() > 0:
67+
sys.exit(0)
68+
69+
# Redirige stdin/stdout/stderr vers /dev/null
70+
devnull = os.open(os.devnull, os.O_RDWR)
71+
for fd in (0, 1, 2):
72+
os.dup2(devnull, fd)
73+
os.close(devnull)
74+
75+
PID_FILE.write_text(str(os.getpid()))
76+
77+
78+
def get_pid() -> int | None:
79+
if PID_FILE.exists():
80+
try:
81+
return int(PID_FILE.read_text().strip())
82+
except ValueError:
83+
return None
84+
return None
85+
86+
87+
def do_run(source: Path, dest: Path):
88+
"""Lance la surveillance en foreground (Ctrl+C pour arrêter)."""
89+
if not source.is_dir():
90+
print(f"Erreur : répertoire source introuvable : {source}")
91+
sys.exit(1)
92+
93+
dest.mkdir(parents=True, exist_ok=True)
94+
setup_logging(foreground=True)
95+
96+
logging.info(f"Surveillance de : {source}")
97+
logging.info(f"Destination : {dest}")
98+
logging.info("Ctrl+C pour arrêter.")
99+
100+
observer = Observer()
101+
observer.schedule(CopyOnCreate(dest), str(source), recursive=True)
102+
observer.start()
103+
104+
try:
105+
observer.join()
106+
except KeyboardInterrupt:
107+
observer.stop()
108+
observer.join()
109+
logging.info("Arrêté.")
110+
111+
112+
def do_start(source: Path, dest: Path):
113+
if get_pid() is not None:
114+
print("Le daemon tourne déjà. Utilisez 'restart' pour le relancer.")
115+
sys.exit(1)
116+
117+
if not source.is_dir():
118+
print(f"Erreur : répertoire source introuvable : {source}")
119+
sys.exit(1)
120+
121+
dest.mkdir(parents=True, exist_ok=True)
122+
123+
print(f"Démarrage du daemon…")
124+
print(f" Source : {source}")
125+
print(f" Destination : {dest}")
126+
print(f" Logs : {LOG_FILE}")
127+
print(f" PID : {PID_FILE}")
128+
129+
daemonize()
130+
setup_logging()
131+
132+
logging.info(f"Daemon démarré (PID {os.getpid()})")
133+
logging.info(f"Surveillance de : {source}")
134+
logging.info(f"Destination : {dest}")
135+
136+
observer = Observer()
137+
observer.schedule(CopyOnCreate(dest), str(source), recursive=True)
138+
observer.start()
139+
140+
def on_signal(signum, frame):
141+
observer.stop()
142+
143+
signal.signal(signal.SIGTERM, on_signal)
144+
signal.signal(signal.SIGINT, on_signal)
145+
146+
observer.join()
147+
PID_FILE.unlink(missing_ok=True)
148+
logging.info("Daemon arrêté.")
149+
150+
151+
def do_stop():
152+
pid = get_pid()
153+
if pid is None:
154+
print("Le daemon n'est pas en cours d'exécution.")
155+
sys.exit(1)
156+
try:
157+
os.kill(pid, signal.SIGTERM)
158+
PID_FILE.unlink(missing_ok=True)
159+
print(f"Daemon arrêté (PID {pid}).")
160+
except ProcessLookupError:
161+
print(f"Processus {pid} introuvable — nettoyage du fichier PID.")
162+
PID_FILE.unlink(missing_ok=True)
163+
164+
165+
def do_status():
166+
pid = get_pid()
167+
if pid is None:
168+
print("Daemon : arrêté")
169+
return
170+
try:
171+
os.kill(pid, 0)
172+
print(f"Daemon : en cours d'exécution (PID {pid})")
173+
print(f"Logs : {LOG_FILE}")
174+
except ProcessLookupError:
175+
print(f"Daemon : arrêté (fichier PID périmé)")
176+
PID_FILE.unlink(missing_ok=True)
177+
178+
179+
USAGE = (
180+
f"Usage: {sys.argv[0]} run [<source> <dest>] # foreground\n"
181+
f" {sys.argv[0]} start [<source> <dest>] # daemon\n"
182+
f" {sys.argv[0]} stop\n"
183+
f" {sys.argv[0]} status\n"
184+
f" {sys.argv[0]} restart [<source> <dest>] # daemon\n"
185+
f"\n"
186+
f"Si <source>/<dest> sont omis, ils sont lus dans {CONFIG_FILE}."
187+
)
188+
189+
190+
def main():
191+
if len(sys.argv) < 2:
192+
print(USAGE)
193+
sys.exit(1)
194+
195+
cmd = sys.argv[1]
196+
197+
if cmd == "stop":
198+
do_stop()
199+
elif cmd == "status":
200+
do_status()
201+
elif cmd in ("run", "start", "restart"):
202+
if len(sys.argv) == 4:
203+
source_raw, dest_raw = sys.argv[2], sys.argv[3]
204+
elif len(sys.argv) == 2:
205+
config = load_config()
206+
source_raw, dest_raw = config.get("source_dir"), config.get("dest_dir")
207+
if not source_raw or not dest_raw:
208+
print(f"Erreur : source_dir/dest_dir manquants dans {CONFIG_FILE}")
209+
sys.exit(1)
210+
else:
211+
print(USAGE)
212+
sys.exit(1)
213+
214+
source = Path(source_raw).resolve()
215+
dest = Path(dest_raw).resolve()
216+
if cmd == "run":
217+
do_run(source, dest)
218+
else:
219+
if cmd == "restart":
220+
do_stop()
221+
do_start(source, dest)
222+
else:
223+
print(USAGE)
224+
sys.exit(1)
225+
226+
227+
if __name__ == "__main__":
228+
main()

watch_and_copy.service

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[Unit]
2+
Description=Watch and copy new files from source_dir to dest_dir
3+
After=network.target
4+
5+
[Service]
6+
Type=forking
7+
User=root
8+
PIDFile=/tmp/watch_and_copy.pid
9+
ExecStart=/usr/bin/watch_and_copy.py start
10+
Restart=on-failure
11+
RestartSec=5
12+
13+
[Install]
14+
WantedBy=multi-user.target

0 commit comments

Comments
 (0)