Setting the file. One moment. Clean · PPTX · anthropics/skills · Skills DocsOpc Dig Sig
- Number
- 11.3
- Position
- 3 of 54
- Type
- Python
- Size
- 10 KB
- Lines
- 309
scripts/clean.py
Python·309 lines·10 KB
17
18import posixpath
19import re
20import sys
21from pathlib import Path
22
23import defusedxml.minidom
24
25from office.helpers import SLIDE_REL_TYPE, opc_target, rels_source_part
26
27
28def _slide_rids(pres_rels_path: Path, unpacked_dir: Path) -> dict[str, str]:
29 source_part = rels_source_part(pres_rels_path, unpacked_dir)
30 rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
31
32 rids: dict[str, str] = {}
33 for rel in rels_dom.getElementsByTagName("Relationship"):
34 if rel.getAttribute("Type") != SLIDE_REL_TYPE:
35 continue
36 part = opc_target(
37 rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode")
38 )
39 if part is not None:
40 rids[rel.getAttribute("Id")] = part
41 return rids
42
43
44def get_slides_in_sldidlst(unpacked_dir: Path) -> set[str]:
45 pres_path = unpacked_dir / "ppt" / "presentation.xml"
46 pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
47
48 if not pres_path.exists() or not pres_rels_path.exists():
49 return set()
50
51 rid_to_slide = _slide_rids(pres_rels_path, unpacked_dir)
52
53 pres_content = pres_path.read_text(encoding="utf-8")
54 referenced_rids = set(re.findall(r'<p:sldId[^>]*r:id="([^"]+)"', pres_content))
55
56 return {
57 posixpath.basename(rid_to_slide[rid])
58 for rid in referenced_rids
59 if rid in rid_to_slide
60 }
61
62
63class RefusedToClean(Exception):
64 """The package does not look the way a readable package should."""
65
66
67def remove_orphaned_slides(unpacked_dir: Path) -> list[str]:
68 slides_dir = unpacked_dir / "ppt" / "slides"
69 slides_rels_dir = slides_dir / "_rels"
70 pres_rels_path = unpacked_dir / "ppt" / "_rels" / "presentation.xml.rels"
71
72 if not slides_dir.exists():
73 return []
74
75 referenced_slides = get_slides_in_sldidlst(unpacked_dir)
76 on_disk = sorted(slides_dir.glob("slide*.xml"))
77
78 if on_disk and not any(s.name in referenced_slides for s in on_disk):
79 listed = re.findall(
80 r'<p:sldId[^>]*r:id="([^"]+)"',
81 (unpacked_dir / "ppt" / "presentation.xml").read_text(encoding="utf-8")
82 if (unpacked_dir / "ppt" / "presentation.xml").exists()
83 else "",
84 )
85 if listed:
86 raise RefusedToClean(
87 f"<p:sldIdLst> lists {len(listed)} slide(s) and none of the "
88 f"{len(on_disk)} slide(s) on disk match any of them. Refusing to "
89 f"delete them all — this is a parse failure, not an empty deck."
90 )
91
92 removed = []
93
94 for slide_file in on_disk:
95 if slide_file.name not in referenced_slides:
96 rel_path = slide_file.relative_to(unpacked_dir)
97 slide_file.unlink()
98 removed.append(str(rel_path))
99
100 rels_file = slides_rels_dir / f"{slide_file.name}.rels"
101 if rels_file.exists():
102 rels_file.unlink()
103 removed.append(str(rels_file.relative_to(unpacked_dir)))
104
105 if removed and pres_rels_path.exists():
106 rels_dom = defusedxml.minidom.parse(str(pres_rels_path))
107 source_part = rels_source_part(pres_rels_path, unpacked_dir)
108 changed = False
109
110 for rel in list(rels_dom.getElementsByTagName("Relationship")):
111 if rel.getAttribute("Type") != SLIDE_REL_TYPE:
112 continue
113 part = opc_target(
114 rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode")
115 )
116 if part is None:
117 continue
118 if posixpath.basename(part) not in referenced_slides:
119 if rel.parentNode:
120 rel.parentNode.removeChild(rel)
121 changed = True
122
123 if changed:
124 with open(pres_rels_path, "wb") as f:
125 f.write(rels_dom.toxml(encoding="utf-8"))
126
127 return removed
128
129
130def remove_trash_directory(unpacked_dir: Path) -> list[str]:
131 trash_dir = unpacked_dir / "[trash]"
132 removed = []
133
134 if trash_dir.exists() and trash_dir.is_dir():
135 for file_path in trash_dir.iterdir():
136 if file_path.is_file():
137 rel_path = file_path.relative_to(unpacked_dir)
138 removed.append(str(rel_path))
139 file_path.unlink()
140 trash_dir.rmdir()
141
142 return removed
143
144
145def _referenced_by(rels_files, unpacked_dir: Path) -> set:
146 referenced = set()
147
148 for rels_file in rels_files:
149 source_part = rels_source_part(rels_file, unpacked_dir)
150 dom = defusedxml.minidom.parse(str(rels_file))
151 for rel in dom.getElementsByTagName("Relationship"):
152 part = opc_target(
153 rel.getAttribute("Target"), source_part, rel.getAttribute("TargetMode")
154 )
155 if part is not None:
156 referenced.add(Path(part))
157
158 return referenced
159
160
161def remove_orphaned_rels_files(unpacked_dir: Path) -> list[str]:
162 resource_dirs = ["charts", "diagrams", "drawings"]
163 removed = []
164
165 for dir_name in resource_dirs:
166 rels_dir = unpacked_dir / "ppt" / dir_name / "_rels"
167 if not rels_dir.exists():
168 continue
169
170 for rels_file in rels_dir.glob("*.rels"):
171 resource_file = rels_dir.parent / rels_file.name.replace(".rels", "")
172 if not resource_file.exists():
173 rels_file.unlink()
174 removed.append(str(rels_file.relative_to(unpacked_dir)))
175
176 return removed
177
178
179def get_referenced_files(unpacked_dir: Path) -> set:
180 return _referenced_by(sorted(unpacked_dir.rglob("*.rels")), unpacked_dir)
181
182
183def remove_orphaned_files(unpacked_dir: Path, referenced: set) -> list[str]:
184 resource_dirs = ["media", "embeddings", "charts", "diagrams", "tags", "drawings", "ink"]
185 removed = []
186
187 for dir_name in resource_dirs:
188 dir_path = unpacked_dir / "ppt" / dir_name
189 if not dir_path.exists():
190 continue
191
192 for file_path in dir_path.glob("*"):
193 if not file_path.is_file():
194 continue
195 rel_path = file_path.relative_to(unpacked_dir)
196 if rel_path not in referenced:
197 file_path.unlink()
198 removed.append(str(rel_path))
199
200 theme_dir = unpacked_dir / "ppt" / "theme"
201 if theme_dir.exists():
202 for file_path in theme_dir.glob("theme*.xml"):
203 rel_path = file_path.relative_to(unpacked_dir)
204 if rel_path not in referenced:
205 file_path.unlink()
206 removed.append(str(rel_path))
207 theme_rels = theme_dir / "_rels" / f"{file_path.name}.rels"
208 if theme_rels.exists():
209 theme_rels.unlink()
210 removed.append(str(theme_rels.relative_to(unpacked_dir)))
211
212 notes_dir = unpacked_dir / "ppt" / "notesSlides"
213 if notes_dir.exists():
214 for file_path in notes_dir.glob("*.xml"):
215 if not file_path.is_file():
216 continue
217 rel_path = file_path.relative_to(unpacked_dir)
218 if rel_path not in referenced:
219 file_path.unlink()
220 removed.append(str(rel_path))
221
222 notes_rels_dir = notes_dir / "_rels"
223 if notes_rels_dir.exists():
224 for file_path in notes_rels_dir.glob("*.rels"):
225 notes_file = notes_dir / file_path.name.replace(".rels", "")
226 if not notes_file.exists():
227 file_path.unlink()
228 removed.append(str(file_path.relative_to(unpacked_dir)))
229
230 return removed
231
232
233def update_content_types(unpacked_dir: Path, removed_files: list[str]) -> None:
234 ct_path = unpacked_dir / "[Content_Types].xml"
235 if not ct_path.exists():
236 return
237
238 dom = defusedxml.minidom.parse(str(ct_path))
239 changed = False
240
241 for override in list(dom.getElementsByTagName("Override")):
242 part_name = override.getAttribute("PartName").lstrip("/")
243 if part_name in removed_files:
244 if override.parentNode:
245 override.parentNode.removeChild(override)
246 changed = True
247
248 if changed:
249 with open(ct_path, "wb") as f:
250 f.write(dom.toxml(encoding="utf-8"))
251
252
253def clean_unused_files(unpacked_dir: Path) -> list[str]:
254 all_removed = []
255
256 if list(unpacked_dir.rglob("*.rels")) and not get_referenced_files(unpacked_dir):
257 raise RefusedToClean(
258 "no relationship in this package names a part we can resolve. "
259 "Refusing to treat every file as unreferenced."
260 )
261
262 slides_removed = remove_orphaned_slides(unpacked_dir)
263 all_removed.extend(slides_removed)
264
265 trash_removed = remove_trash_directory(unpacked_dir)
266 all_removed.extend(trash_removed)
267
268 while True:
269 removed_rels = remove_orphaned_rels_files(unpacked_dir)
270 referenced = get_referenced_files(unpacked_dir)
271 removed_files = remove_orphaned_files(unpacked_dir, referenced)
272
273 total_removed = removed_rels + removed_files
274 if not total_removed:
275 break
276
277 all_removed.extend(total_removed)
278
279 if all_removed:
280 update_content_types(unpacked_dir, all_removed)
281
282 return all_removed
283
284
285if __name__ == "__main__":
286 if len(sys.argv) != 2:
287 print("Usage: python clean.py <unpacked_dir>", file=sys.stderr)
288 print("Example: python clean.py unpacked/", file=sys.stderr)
289 sys.exit(1)
290
291 unpacked_dir = Path(sys.argv[1])
292
293 if not unpacked_dir.exists():
294 print(f"Error: {unpacked_dir} not found", file=sys.stderr)
295 sys.exit(1)
296
297 try:
298 removed = clean_unused_files(unpacked_dir)
299 except (RefusedToClean, ValueError) as e:
300 print(f"Error: {e}", file=sys.stderr)
301 print("Nothing was deleted.", file=sys.stderr)
302 sys.exit(1)
303
304 if removed:
305 print(f"Removed {len(removed)} unreferenced files:")
306 for f in removed:
307 print(f" {f}")
308 else:
309 print("No unreferenced files found")