Coverage for dak/archive_dedup_pool.py: 56%
53 statements
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
« prev ^ index » next coverage.py v7.6.0, created at 2026-08-03 16:46 +0000
1"""De-duplicates files in the pool directory
3@contact: Debian FTP Master <ftpmaster@debian.org>
4@copyright: 2017 Bastian Blank <waldi@debian.org>
5@license: GNU General Public License version 2 or later
6"""
8# This program is free software; you can redistribute it and/or modify
9# it under the terms of the GNU General Public License as published by
10# the Free Software Foundation; either version 2 of the License, or
11# (at your option) any later version.
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16# GNU General Public License for more details.
18# You should have received a copy of the GNU General Public License
19# along with this program; if not, write to the Free Software
20# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22################################################################################
24import errno
25import os
26import sys
27from typing import TYPE_CHECKING
29import apt_pkg
30from sqlalchemy import sql
32from daklib import daklog
33from daklib.config import Config
34from daklib.dbconn import DBConn
36if TYPE_CHECKING:
37 from sqlalchemy.orm import Session
39Options: apt_pkg.Configuration
40Logger: daklog.Logger
42################################################################################
43################################################################################
44################################################################################
47def usage(exit_code=0):
48 print(
49 """Usage: dak archive-dedup-pool [OPTION]...
50 -h, --help show this help and exit.
51 -V, --version display the version number and exit
52"""
53 )
54 sys.exit(exit_code)
57################################################################################
60def dedup_one(size: int, reference: str, *filenames: str) -> None:
61 stat_reference = os.stat(reference)
63 # safety net
64 if stat_reference.st_size != size:
65 raise RuntimeError(
66 "Size of {} does not match database: {} != {}".format(
67 reference, size, stat_reference.st_size
68 )
69 )
71 for filename in filenames:
72 stat_filename = os.stat(filename)
74 # if file is already a hard-linked, ignore
75 if stat_reference == stat_filename:
76 continue
78 # safety net
79 if stat_filename.st_size != size:
80 raise RuntimeError(
81 "Size of {} does not match database: {} != {}".format(
82 filename, size, stat_filename.st_size
83 )
84 )
86 tempfile = filename + ".new"
87 os.link(reference, tempfile)
88 try:
89 Logger.log(["deduplicate", filename, reference])
90 os.rename(tempfile, filename)
91 finally:
92 try:
93 os.unlink(tempfile)
94 except OSError as e:
95 if e.errno != errno.ENOENT:
96 raise
99################################################################################
102def dedup(session: "Session") -> None:
103 results = session.execute(
104 sql.text(
105 """
106SELECT DISTINCT *
107 FROM (
108 SELECT
109 f.size,
110 array_agg(a.path || '/pool/' || c.name || '/' || f.filename) OVER (
111 -- we aggregate all files with the same size, sha256sum and archive
112 PARTITION BY f.size, f.sha256sum, a.id
113 -- the oldest should be first
114 ORDER by f.created
115 -- we always want to see all rows
116 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
117 )
118 AS filenames
119 FROM
120 files AS f INNER JOIN
121 files_archive_map AS fa ON f.id = fa.file_id INNER JOIN
122 component c ON fa.component_id = c.id INNER JOIN
123 archive a ON fa.archive_id = a.id
124 ) AS f
125 -- we only care about entries with more than one filename
126 WHERE array_length(filenames, 1) > 1
127 """
128 )
129 ).mappings()
131 for i in results: 131 ↛ 132line 131 didn't jump to line 132 because the loop on line 131 never started
132 dedup_one(i["size"], *i["filenames"])
135################################################################################
138def main():
139 global Options, Logger
141 cnf = Config()
142 session = DBConn().session()
144 Arguments = [("h", "help", "Archive-Dedup-Pool::Options::Help")]
146 apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) # type: ignore[attr-defined]
148 for i in ["help"]:
149 key = "Archive-Dedup-Pool::Options::%s" % i
150 if key not in cnf:
151 cnf[key] = ""
153 Options = cnf.subtree("Archive-Dedup-Pool::Options")
155 if Options["Help"]:
156 usage()
158 Logger = daklog.Logger("archive-dedup-pool")
160 dedup(session)
162 Logger.close()