euler314 commited on
Commit
428bf45
·
verified ·
1 Parent(s): fb70a1b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +135 -79
app.py CHANGED
@@ -1,112 +1,168 @@
1
  """
2
- Streamlit File Extension Renamer
3
- --------------------------------
4
- Lightweight Streamlit app for Hugging Face Spaces.
5
-
6
- Updates (2025‑05‑22):
7
- * Ensures Streamlit has a writable config dir (`STREAMLIT_HOME`) inside container.
8
- * Sets a fallback `HOME` env so `~/.streamlit` resolves safely.
9
- * Compatible with Streamlit >=1.45.1 (bug‑fixed event‑loop).
 
 
 
 
10
  """
11
  from __future__ import annotations
12
 
13
- import os
14
- import pathlib
 
 
 
 
15
  import io
16
  import zipfile
17
  from datetime import datetime
18
  from pathlib import Path
19
 
20
- # ---------------------------------------------------------------------
21
- # Environment hardening (MUST run before `import streamlit`)
22
- # ---------------------------------------------------------------------
23
- os.environ.setdefault("STREAMLIT_HOME", "/tmp/.streamlit")
24
- os.environ.setdefault("HOME", "/tmp")
25
- pathlib.Path(os.environ["STREAMLIT_HOME"]).mkdir(parents=True, exist_ok=True)
26
-
27
  import streamlit as st
28
-
29
- # ---------------------------------------------------------------------
30
- # Configuration
31
- # ---------------------------------------------------------------------
32
- ALLOWED_TARGET_EXTS: list[str] = [
33
- ".txt", ".pdf", ".doc", ".docx", ".json", ".csv", ".xml", ".html",
34
- ".css", ".js", ".md", ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff",
35
- ".svg", ".ico", ".mp3", ".wav", ".mp4", ".avi", ".mkv", ".mov",
36
- ".zip", ".tar", ".gz", ".7z",
37
- ]
38
-
39
- DISALLOWED_SOURCE_EXTS: set[str] = {".exe", ".bin"}
40
-
41
- # ---------------------------------------------------------------------
42
- # UI helpers
43
- # ---------------------------------------------------------------------
44
-
45
- def make_sidebar() -> str:
 
 
 
 
 
 
 
 
46
  st.sidebar.header("Settings")
 
 
 
 
47
  target_ext = st.sidebar.selectbox(
48
- "Target extension (applied to **all** uploaded files)",
49
- options=ALLOWED_TARGET_EXTS,
50
- index=ALLOWED_TARGET_EXTS.index(".pdf"),
 
51
  )
52
  st.sidebar.markdown(
53
- " **Note**: This tool only renames file extensions. It does **not** "
54
- "convert the underlying format."
55
  )
56
  return target_ext
57
 
58
 
59
- def make_uploader():
60
  return st.file_uploader(
61
- "Upload one or more files",
62
  accept_multiple_files=True,
63
- type=[ext.lstrip(".") for ext in ALLOWED_TARGET_EXTS],
64
- help="Drag‑and‑drop or browse for files to rename.",
65
  )
66
 
67
- # ---------------------------------------------------------------------
68
- # Core logic
69
- # ---------------------------------------------------------------------
70
 
71
- def write_zip(uploaded_files: list[st.runtime.uploaded_file_manager.UploadedFile],
72
- target_ext: str) -> io.BytesIO:
73
- buffer = io.BytesIO()
74
- with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as zf:
75
- for file in uploaded_files:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  orig_path = Path(file.name)
77
  if orig_path.suffix.lower() in DISALLOWED_SOURCE_EXTS:
78
  st.warning(f"⏭️ Skipping disallowed file: **{orig_path.name}**")
79
  continue
80
- renamed = orig_path.with_suffix(target_ext)
81
- zf.writestr(renamed.name, file.read())
82
- st.success(f"✅ Renamed **{orig_path.name}** → **{renamed.name}**")
83
- buffer.seek(0)
84
- return buffer
85
-
86
- # ---------------------------------------------------------------------
87
- # Main
88
- # ---------------------------------------------------------------------
89
-
90
- def main() -> None:
91
- st.set_page_config("Extension Renamer", page_icon="📄", layout="centered")
92
- st.title("📄 Universal File‑Extension Renamer")
93
- st.write("Upload files, choose a new extension, and download them renamed "
94
- "in a ZIP archive.")
95
-
96
- target_ext = make_sidebar()
97
- uploaded_files = make_uploader()
98
-
99
- if uploaded_files and st.button("🔄 Rename & Package"):
100
- zip_buffer = write_zip(uploaded_files, target_ext)
101
- timestamp = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
 
102
  st.download_button(
103
- label="⬇️ Download ZIP",
104
- data=zip_buffer,
105
- file_name=f"renamed_{timestamp}.zip",
106
- mime="application/zip",
107
  )
108
 
109
- st.caption("© 2025 File‑Extension Renamer Streamlit Hugging Face Spaces")
 
110
 
111
  if __name__ == "__main__":
112
  main()
 
1
  """
2
+ Streamlit Universal File‑Format Changer
3
+ --------------------------------------
4
+ A Streamlit app ready for Hugging Face Spaces that **actually converts file
5
+ contents when possible**, instead of merely renaming extensions.
6
+
7
+ * Image ↔ image conversions via **Pillow** (JPEG, PNG, GIF, BMP, TIFF, ICO, WEBP)
8
+ * Plain‑text files kept intact but re‑encoded (UTF‑8) when changing among
9
+ text‑like extensions (txt, md, csv, json, xml, html, css, js)
10
+ * Disallowed uploads: `.exe`, `.bin`
11
+ * Everything is bundled into one ZIP download.
12
+
13
+ Created 2025‑05‑22 • v2
14
  """
15
  from __future__ import annotations
16
 
17
+ # NOTE: Set env vars *before* importing Streamlit ------------------------------
18
+ import os, pathlib
19
+ os.environ.setdefault("STREAMLIT_HOME", "/tmp/.streamlit")
20
+ os.environ.setdefault("HOME", "/tmp")
21
+ pathlib.Path(os.environ["STREAMLIT_HOME"]).mkdir(parents=True, exist_ok=True)
22
+
23
  import io
24
  import zipfile
25
  from datetime import datetime
26
  from pathlib import Path
27
 
 
 
 
 
 
 
 
28
  import streamlit as st
29
+ from PIL import Image # Pillow for real image conversion
30
+
31
+ # -----------------------------------------------------------------------------
32
+ # Supported extensions ---------------------------------------------------------
33
+ # -----------------------------------------------------------------------------
34
+ IMAGE_EXTS = {
35
+ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".ico", ".webp",
36
+ }
37
+ TEXT_EXTS = {
38
+ ".txt", ".md", ".csv", ".json", ".xml", ".html", ".css", ".js",
39
+ }
40
+ ARCHIVE_EXTS = {".zip", ".tar", ".gz", ".7z"}
41
+ MEDIA_EXTS = {".mp3", ".wav", ".mp4", ".avi", ".mkv", ".mov"}
42
+ DOC_EXTS = {".pdf", ".doc", ".docx"}
43
+
44
+ ALLOWED_TARGET_EXTS: list[str] = sorted(
45
+ IMAGE_EXTS | TEXT_EXTS | ARCHIVE_EXTS | MEDIA_EXTS | DOC_EXTS
46
+ )
47
+
48
+ DISALLOWED_SOURCE_EXTS = {".exe", ".bin"}
49
+
50
+ # -----------------------------------------------------------------------------
51
+ # Helpers ----------------------------------------------------------------------
52
+ # -----------------------------------------------------------------------------
53
+
54
+ def sidebar_target_extension() -> str:
55
  st.sidebar.header("Settings")
56
+ query = st.sidebar.text_input("Filter extensions… (optional)")
57
+ filtered = [e for e in ALLOWED_TARGET_EXTS if query.lower() in e]
58
+ if not filtered:
59
+ st.sidebar.error("No extension matches that filter.")
60
  target_ext = st.sidebar.selectbox(
61
+ "Choose target extension (applied to **all** files)",
62
+ filtered or ALLOWED_TARGET_EXTS,
63
+ index=(filtered or ALLOWED_TARGET_EXTS).index(".png")
64
+ if ".png" in (filtered or ALLOWED_TARGET_EXTS) else 0,
65
  )
66
  st.sidebar.markdown(
67
+ "*Images are truly converted. Text files are re‑saved as UTF‑8. "
68
+ "Other combinations fall back to a safe rename.*"
69
  )
70
  return target_ext
71
 
72
 
73
+ def uploader():
74
  return st.file_uploader(
75
+ "Upload any files (multiple allowed)",
76
  accept_multiple_files=True,
77
+ type=None, # accept *all* extensions
78
+ help="Drag‑and‑drop or click to browse.",
79
  )
80
 
 
 
 
81
 
82
+ # -----------------------------------------------------------------------------
83
+ # Conversion logic -------------------------------------------------------------
84
+ # -----------------------------------------------------------------------------
85
+
86
+ def convert_image(data: bytes, target_ext: str) -> bytes:
87
+ """Return `bytes` of the image converted to `target_ext`. Raises if Pillow
88
+ cannot save in that format."""
89
+ img = Image.open(io.BytesIO(data))
90
+ buf = io.BytesIO()
91
+ # Map certain extensions to Pillow format names
92
+ pil_fmt = {
93
+ ".jpg": "JPEG", ".jpeg": "JPEG", ".png": "PNG", ".gif": "GIF",
94
+ ".bmp": "BMP", ".tiff": "TIFF", ".ico": "ICO", ".webp": "WEBP",
95
+ }[target_ext]
96
+ img.save(buf, format=pil_fmt)
97
+ buf.seek(0)
98
+ return buf.read()
99
+
100
+
101
+ def convert_text(data: bytes, _target_ext: str) -> bytes:
102
+ """Return data re‑encoded as UTF‑8 (no format change)."""
103
+ text = data.decode("utf‑8", errors="ignore")
104
+ return text.encode("utf‑8")
105
+
106
+
107
+ def convert_file(file: st.runtime.uploaded_file_manager.UploadedFile, target_ext: str) -> tuple[bytes, str]:
108
+ """Try to convert and return (bytes, conversion_note). On failure, return
109
+ original data with a note that only rename happened."""
110
+ orig_ext = Path(file.name).suffix.lower()
111
+ raw = file.read()
112
+
113
+ try:
114
+ if orig_ext in IMAGE_EXTS and target_ext in IMAGE_EXTS:
115
+ return convert_image(raw, target_ext), "image converted"
116
+ if orig_ext in TEXT_EXTS and target_ext in TEXT_EXTS:
117
+ return convert_text(raw, target_ext), "text re‑encoded"
118
+ except Exception as err:
119
+ st.warning(f"⚠️ Could not convert **{file.name}**: {err}. Falling back to rename.")
120
+
121
+ # Fallback: no conversion
122
+ return raw, "renamed only"
123
+
124
+
125
+ # -----------------------------------------------------------------------------
126
+ # Zip packaging ---------------------------------------------------------------
127
+ # -----------------------------------------------------------------------------
128
+
129
+ def package_zip(files: list[st.runtime.uploaded_file_manager.UploadedFile], target_ext: str) -> io.BytesIO:
130
+ buf = io.BytesIO()
131
+ with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
132
+ for file in files:
133
  orig_path = Path(file.name)
134
  if orig_path.suffix.lower() in DISALLOWED_SOURCE_EXTS:
135
  st.warning(f"⏭️ Skipping disallowed file: **{orig_path.name}**")
136
  continue
137
+ data, note = convert_file(file, target_ext)
138
+ new_name = orig_path.with_suffix(target_ext).name
139
+ zf.writestr(new_name, data)
140
+ st.success(f"✅ {note} • **{orig_path.name}** → **{new_name}**")
141
+ buf.seek(0)
142
+ return buf
143
+
144
+
145
+ # -----------------------------------------------------------------------------
146
+ # Main ------------------------------------------------------------------------
147
+ # -----------------------------------------------------------------------------
148
+
149
+ def main():
150
+ st.set_page_config("Universal Format Changer", page_icon="🔄", layout="centered")
151
+ st.title("🔄 Universal File‑Format Changer")
152
+ st.write("Upload files, pick a target extension, and download a ZIP with the converted files.")
153
+
154
+ target_ext = sidebar_target_extension()
155
+ files = uploader()
156
+
157
+ if files and st.button("🚀 Convert & Download"):
158
+ zip_buf = package_zip(files, target_ext)
159
+ ts = datetime.utcnow().strftime("%Y%m%dT%H%M%SZ")
160
  st.download_button(
161
+ "⬇️ Get ZIP", zip_buf, file_name=f"converted_{ts}.zip", mime="application/zip"
 
 
 
162
  )
163
 
164
+ st.caption("© 2025 Universal Changer  Streamlit  Hugging Face Spaces")
165
+
166
 
167
  if __name__ == "__main__":
168
  main()