infra: uploader → post wizard (auto-naming, markdown template, fields output)

This commit is contained in:
Telegram Bot
2026-04-29 13:38:08 +03:00
parent 5f99a0460c
commit 943a111af5
+316 -115
View File
@@ -1,15 +1,14 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os, tempfile import os, tempfile, re
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
import boto3 import boto3
from botocore.client import Config from botocore.client import Config
from botocore.exceptions import ClientError
import pillow_heif import pillow_heif
from PIL import Image from PIL import Image
pillow_heif.register_heif_opener() pillow_heif.register_heif_opener()
app = Flask(__name__) app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024
S3_CONFIG = dict( S3_CONFIG = dict(
endpoint_url='https://s3.regru.cloud', endpoint_url='https://s3.regru.cloud',
@@ -19,148 +18,344 @@ S3_CONFIG = dict(
config=Config(signature_version='s3v4') config=Config(signature_version='s3v4')
) )
BUCKET = 'sleeptrip-dev' BUCKET = 'sleeptrip-dev'
PREFIX = 'images/'
ALLOWED = {'jpg', 'jpeg', 'png', 'gif', 'webp', 'mp4', 'mov', 'heic', 'heif'}
MIME = { MIME = {
'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'jpg':'image/jpeg','jpeg':'image/jpeg','png':'image/png',
'gif': 'image/gif', 'webp': 'image/webp', 'gif':'image/gif','webp':'image/webp','mp4':'video/mp4','mov':'video/quicktime',
'mp4': 'video/mp4', 'mov': 'video/quicktime', 'heic':'image/jpeg','heif':'image/jpeg'
'heic': 'image/jpeg', 'heif': 'image/jpeg'
} }
ALLOWED = set(MIME.keys())
PAGE = """<!DOCTYPE html> PAGE = r"""<!DOCTYPE html>
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1"> <meta name="viewport" content="width=device-width,initial-scale=1">
<title>Upload to S3</title> <title>PTP Post Wizard</title>
<style> <style>
*{box-sizing:border-box;margin:0;padding:0} *{box-sizing:border-box;margin:0;padding:0}
body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,monospace;display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px} body{background:#0d1117;color:#e6edf3;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;padding:0 0 60px}
.card{background:#161b22;border:1px solid #30363d;border-radius:12px;padding:32px;width:100%;max-width:520px} .top{background:#161b22;border-bottom:1px solid #30363d;padding:16px 24px;display:flex;align-items:center;gap:12px}
h1{font-size:16px;font-weight:600;margin-bottom:4px} .top h1{font-size:15px;font-weight:600}
.sub{font-size:12px;color:#8b949e;margin-bottom:24px} .top .sub{font-size:12px;color:#8b949e}
.drop{border:2px dashed #30363d;border-radius:8px;padding:48px 24px;text-align:center;cursor:pointer;transition:all .2s} .wrap{max-width:860px;margin:0 auto;padding:24px}
.drop.over{border-color:#58a6ff;background:rgba(88,166,255,.05)} .section{margin-bottom:24px}
.drop input{display:none} .section-title{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#8b949e;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #21262d}
.drop-icon{font-size:36px;margin-bottom:12px} .grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
label{display:block;font-size:11px;color:#8b949e;margin-bottom:4px}
input,textarea{width:100%;background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px 10px;color:#e6edf3;font-size:13px;font-family:inherit;outline:none;transition:border-color .15s}
input:focus,textarea:focus{border-color:#58a6ff}
.hint{font-size:11px;color:#8b949e;margin-top:4px}
.slug-preview{font-family:monospace;font-size:12px;color:#58a6ff;margin-top:4px}
.drop{border:2px dashed #30363d;border-radius:10px;padding:40px 24px;text-align:center;cursor:pointer;transition:all .2s;position:relative}
.drop.over,.drop.has-files{border-color:#58a6ff;background:rgba(88,166,255,.04)}
.drop-icon{font-size:32px;margin-bottom:10px;color:#8b949e}
.drop-text{font-size:13px;color:#8b949e} .drop-text{font-size:13px;color:#8b949e}
.drop-text b{color:#58a6ff;cursor:pointer} .drop-text b{color:#58a6ff;cursor:pointer}
.progress{margin-top:16px;display:none} .drop input{display:none}
.bar-wrap{background:#21262d;border-radius:4px;height:6px;margin-top:8px} .photos{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;margin-top:16px}
.bar{background:#58a6ff;height:6px;border-radius:4px;width:0;transition:width .15s} .photo-card{background:#161b22;border:1px solid #30363d;border-radius:8px;overflow:hidden;position:relative}
.status{font-size:12px;color:#8b949e;margin-top:6px} .photo-card img{width:100%;height:110px;object-fit:cover;display:block}
.result{margin-top:20px;display:none} .photo-card .ph-name{font-size:10px;font-family:monospace;padding:6px 8px;color:#58a6ff;word-break:break-all;line-height:1.4}
.url-box{background:#0d1117;border:1px solid #3fb950;border-radius:6px;padding:12px 14px;font-size:12px;font-family:monospace;word-break:break-all;color:#3fb950;cursor:pointer} .photo-card .ph-num{position:absolute;top:6px;left:6px;background:rgba(0,0,0,.7);color:#fff;font-size:11px;font-weight:700;padding:2px 7px;border-radius:4px}
.url-box:hover{background:rgba(63,185,80,.05)} .photo-card .ph-del{position:absolute;top:6px;right:6px;background:rgba(248,81,73,.8);color:#fff;border:none;border-radius:4px;padding:2px 7px;font-size:11px;cursor:pointer}
.copy-hint{font-size:11px;color:#8b949e;margin-top:6px} .photo-card.uploading{opacity:.6}
.preview{margin-top:12px;max-width:100%;max-height:200px;border-radius:6px;display:none} .photo-card.uploading::after{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:32px;color:#58a6ff}
.err{color:#f85149;font-size:12px;margin-top:12px;display:none;padding:8px 12px;background:rgba(248,81,73,.08);border-radius:6px} .output{background:#161b22;border:1px solid #30363d;border-radius:10px;overflow:hidden}
.hist{margin-top:24px;border-top:1px solid #21262d;padding-top:16px;display:none} .out-tabs{display:flex;border-bottom:1px solid #30363d}
.hist h2{font-size:11px;text-transform:uppercase;letter-spacing:1px;color:#8b949e;margin-bottom:10px} .out-tab{padding:10px 18px;font-size:12px;color:#8b949e;cursor:pointer;border-bottom:2px solid transparent;transition:all .15s}
.hi{font-size:11px;font-family:monospace;color:#8b949e;padding:5px 0;border-bottom:1px solid #21262d;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .out-tab.active{color:#58a6ff;border-bottom-color:#58a6ff}
.hi:hover{color:#58a6ff} .out-panel{display:none;padding:16px}
.out-panel.active{display:block}
.out-panel pre{background:#0d1117;border-radius:6px;padding:14px;font-size:12px;font-family:monospace;overflow-x:auto;white-space:pre-wrap;word-break:break-all;color:#e6edf3;max-height:400px;overflow-y:auto}
.copy-btn{background:#21262d;border:1px solid #30363d;border-radius:6px;padding:6px 14px;font-size:12px;color:#8b949e;cursor:pointer;float:right;margin-bottom:8px;transition:all .15s}
.copy-btn:hover{color:#e6edf3;border-color:#8b949e}
.copy-btn.ok{color:#3fb950;border-color:#3fb950}
.err{color:#f85149;font-size:12px;padding:8px 12px;background:rgba(248,81,73,.08);border-radius:6px;margin-top:8px;display:none}
.prog-bar{height:3px;background:#21262d;border-radius:2px;margin-top:4px;display:none}
.prog-bar-fill{height:3px;background:#58a6ff;border-radius:2px;width:0;transition:width .1s}
.notice{background:rgba(88,166,255,.08);border:1px solid rgba(88,166,255,.2);border-radius:6px;padding:10px 14px;font-size:12px;color:#8b949e;margin-bottom:16px}
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="top">
<h1>Upload &rarr; S3</h1> <div>
<p class="sub">sleeptrip-dev &middot; s3.regru.cloud/images/</p> <h1>PTP Post Wizard</h1>
<div class="drop" id="drop"> <div class="sub">Загрузка фото → готовый пост для Sveltia CMS</div>
<input type="file" id="fi" accept="image/*,video/*" multiple>
<div class="drop-icon">&#8679;</div>
<div class="drop-text">Перетащи или <b id="btn">выбери файл</b></div>
</div>
<div class="progress" id="prog">
<div class="bar-wrap"><div class="bar" id="bar"></div></div>
<div class="status" id="stat">Загружаем...</div>
</div>
<div class="err" id="err"></div>
<div class="result" id="res">
<div class="url-box" id="url" onclick="copy(this)"></div>
<div class="copy-hint" id="hint">Нажми чтобы скопировать</div>
<img class="preview" id="prev">
</div>
<div class="hist" id="hist">
<h2>История</h2>
<div id="hl"></div>
</div> </div>
</div> </div>
<div class="wrap">
<!-- STEP 1: INFO -->
<div class="section">
<div class="section-title">1. Информация о посте</div>
<div class="grid2" style="margin-bottom:12px">
<div>
<label>Заголовок</label>
<input type="text" id="title" placeholder="Дюкинский заказник" oninput="onTitleInput()">
<div class="hint">Название места — как на сайте</div>
</div>
<div>
<label>Slug (URL страницы и база имён фото)</label>
<input type="text" id="slug" placeholder="Dukyn2504" oninput="onSlugInput()">
<div class="slug-preview" id="slug-preview"></div>
</div>
</div>
<div class="grid3">
<div>
<label>Дата поездки</label>
<input type="date" id="date" oninput="updateOutput()">
<div class="hint" id="date-fmt"></div>
</div>
<div>
<label>Описание (анонс)</label>
<input type="text" id="desc" placeholder="скалы, лес, отличная погода" oninput="updateOutput()">
</div>
<div>
<label>Disqus ID</label>
<input type="number" id="disqus" placeholder="134" min="1" oninput="updateOutput()">
<div class="hint">Следующий свободный: 134</div>
</div>
</div>
</div>
<!-- STEP 2: UPLOAD -->
<div class="section">
<div class="section-title">2. Фотографии</div>
<div class="notice">Фото автоматически называются <b id="naming-hint">slug-YYYYMMDD-N.jpg</b> и загружаются в S3. Первое фото = обложка.</div>
<div class="drop" id="drop">
<input type="file" id="fi" accept="image/*,video/*" multiple>
<div class="drop-icon">⬆</div>
<div class="drop-text">Перетащи фото сюда или <b onclick="document.getElementById('fi').click()">выбери файлы</b></div>
</div>
<div class="prog-bar" id="prog"><div class="prog-bar-fill" id="prog-fill"></div></div>
<div class="err" id="err"></div>
<div class="photos" id="photos"></div>
</div>
<!-- STEP 3: OUTPUT -->
<div class="section" id="output-section" style="display:none">
<div class="section-title">3. Готовый контент — копируй в Sveltia</div>
<div class="output">
<div class="out-tabs">
<div class="out-tab active" onclick="switchTab(0)">Поля формы</div>
<div class="out-tab" onclick="switchTab(1)">Markdown контент</div>
<div class="out-tab" onclick="switchTab(2)">Все URL фото</div>
</div>
<div class="out-panel active" id="tab0">
<button class="copy-btn" onclick="copyPanel('fields-out','this')">Копировать</button>
<pre id="fields-out"></pre>
</div>
<div class="out-panel" id="tab1">
<button class="copy-btn" onclick="copyPanel('md-out','this')">Копировать</button>
<pre id="md-out"></pre>
</div>
<div class="out-panel" id="tab2">
<button class="copy-btn" onclick="copyPanel('urls-out','this')">Копировать</button>
<pre id="urls-out"></pre>
</div>
</div>
</div>
</div>
<script> <script>
var urls = []; // ---- State ----
var photos = []; // {url, filename, ext}
var counter = 0;
var uploading = 0;
// ---- Transliteration ----
var RU = {'а':'a','б':'b','в':'v','г':'g','д':'d','е':'e','ё':'yo','ж':'zh','з':'z','и':'i','й':'y','к':'k','л':'l','м':'m','н':'n','о':'o','п':'p','р':'r','с':'s','т':'t','у':'u','ф':'f','х':'kh','ц':'ts','ч':'ch','ш':'sh','щ':'sch','ъ':'','ы':'y','ь':'','э':'e','ю':'yu','я':'ya'};
function translit(s){
return s.toLowerCase().split('').map(function(c){return RU[c]||c;}).join('').replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'');
}
function onTitleInput(){
var slug = document.getElementById('slug');
if(!slug._manual){
var t = translit(document.getElementById('title').value);
slug.value = t;
}
onSlugInput();
}
function onSlugInput(){
var slug = document.getElementById('slug');
slug._manual = slug.value.length > 0;
updateNamingHint();
updateOutput();
}
function getSlug(){ return document.getElementById('slug').value.trim() || 'slug'; }
function getDateStr(){
var d = document.getElementById('date').value;
return d ? d.replace(/-/g,'') : 'YYYYMMDD';
}
function getDateISO(){
var d = document.getElementById('date').value;
return d ? d + 'T00:00:00' : '';
}
function updateNamingHint(){
var s = getSlug(); var d = getDateStr();
document.getElementById('naming-hint').textContent = s+'-'+d+'-N.jpg';
var p = document.getElementById('slug-preview');
p.textContent = s ? 'URL: /post/'+s+'/' : '';
}
// init date = today
(function(){
var d = new Date();
var iso = d.toISOString().slice(0,10);
document.getElementById('date').value = iso;
updateNamingHint();
})();
// ---- Drag & Drop ----
var drop = document.getElementById('drop'); var drop = document.getElementById('drop');
var fi = document.getElementById('fi'); var fi = document.getElementById('fi');
document.getElementById('btn').onclick = function(){ fi.click(); }; drop.addEventListener('dragover',function(e){e.preventDefault();drop.classList.add('over');});
drop.addEventListener('dragover', function(e){ e.preventDefault(); drop.classList.add('over'); }); drop.addEventListener('dragleave',function(){drop.classList.remove('over');});
drop.addEventListener('dragleave', function(){ drop.classList.remove('over'); }); drop.addEventListener('drop',function(e){
drop.addEventListener('drop', function(e){ e.preventDefault(); drop.classList.remove('over'); for(var i=0;i<e.dataTransfer.files.length;i++) doUpload(e.dataTransfer.files[i]); }); e.preventDefault(); drop.classList.remove('over');
fi.addEventListener('change', function(){ for(var i=0;i<fi.files.length;i++) doUpload(fi.files[i]); }); var files = e.dataTransfer.files;
for(var i=0;i<files.length;i++) uploadFile(files[i]);
});
fi.addEventListener('change',function(){
for(var i=0;i<fi.files.length;i++) uploadFile(fi.files[i]);
fi.value='';
});
function uploadFile(file){
var slug = getSlug(); var dateStr = getDateStr();
counter++;
var n = counter;
var ext = file.name.split('.').pop().toLowerCase();
var outExt = (ext==='heic'||ext==='heif') ? 'jpg' : ext;
var filename = slug+'-'+dateStr+'-'+n+'.'+outExt;
// Placeholder card
var card = document.createElement('div');
card.className='photo-card uploading';
card.id='card-'+n;
card.innerHTML='<div style="height:110px;background:#21262d"></div><div class="ph-name">'+filename+'</div><div class="ph-num">#'+n+'</div>';
document.getElementById('photos').appendChild(card);
drop.classList.add('has-files');
function doUpload(file) {
var prog = document.getElementById('prog'); var prog = document.getElementById('prog');
var res = document.getElementById('res'); prog.style.display='block';
var err = document.getElementById('err');
prog.style.display = 'block'; uploading++;
res.style.display = 'none';
err.style.display = 'none';
document.getElementById('bar').style.width = '0';
document.getElementById('stat').textContent = file.name + ' ...';
var fd = new FormData(); var fd = new FormData();
fd.append('file', file); fd.append('file', file);
fd.append('filename', filename);
var xhr = new XMLHttpRequest(); var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function(e){ if(e.lengthComputable) document.getElementById('bar').style.width = (e.loaded/e.total*100)+'%'; }; xhr.upload.onprogress = function(e){
if(e.lengthComputable) document.getElementById('prog-fill').style.width=(e.loaded/e.total*100)+'%';
};
xhr.onload = function(){ xhr.onload = function(){
prog.style.display = 'none'; uploading--;
if(xhr.status === 200){ if(uploading===0){prog.style.display='none'; document.getElementById('prog-fill').style.width='0';}
if(xhr.status===200){
var r = JSON.parse(xhr.responseText); var r = JSON.parse(xhr.responseText);
showRes(r.url, file.type.indexOf('image')===0); photos.push({url:r.url, filename:filename, ext:outExt, n:n});
photos.sort(function(a,b){return a.n-b.n;});
// Update card
var c = document.getElementById('card-'+n);
c.className='photo-card';
c.innerHTML='<img src="'+r.url+'" onerror="this.style.display=\'none\'">'
+'<div class="ph-num">#'+n+'</div>'
+'<button class="ph-del" onclick="deletePhoto('+n+')">✕</button>'
+'<div class="ph-name">'+filename+'</div>';
updateOutput();
} else { } else {
var msg = 'Ошибка '+xhr.status; var msg='Ошибка'; try{msg=JSON.parse(xhr.responseText).error;}catch(e){}
try{ msg = JSON.parse(xhr.responseText).error; }catch(e){} var c=document.getElementById('card-'+n); if(c) c.remove();
showErr(msg); showErr(msg); counter--;
} }
}; };
xhr.onerror = function(){ prog.style.display='none'; showErr('Сетевая ошибка'); }; xhr.onerror=function(){uploading--;showErr('Сетевая ошибка');};
xhr.open('POST', '/upload/api'); xhr.open('POST','/upload/api');
xhr.withCredentials = true; xhr.withCredentials=true;
xhr.send(fd); xhr.send(fd);
} }
function showRes(url, isImg) { function deletePhoto(n){
var el = document.getElementById('url'); photos = photos.filter(function(p){return p.n!==n;});
el.textContent = url; var c=document.getElementById('card-'+n); if(c) c.remove();
document.getElementById('res').style.display = 'block'; if(photos.length===0) document.getElementById('output-section').style.display='none';
document.getElementById('hint').textContent = 'Нажми чтобы скопировать'; updateOutput();
var p = document.getElementById('prev');
if(isImg){ p.src=url; p.style.display='block'; } else { p.style.display='none'; }
urls.unshift(url);
updateHist();
} }
function showErr(msg){ var e=document.getElementById('err'); e.textContent=msg; e.style.display='block'; } function showErr(msg){var e=document.getElementById('err');e.textContent=msg;e.style.display='block';setTimeout(function(){e.style.display='none';},5000);}
function copy(el){ // ---- Output generation ----
navigator.clipboard.writeText(el.textContent).then(function(){ function updateOutput(){
document.getElementById('hint').textContent='Скопировано!'; updateNamingHint();
setTimeout(function(){ document.getElementById('hint').textContent='Нажми чтобы скопировать'; }, 2000); if(photos.length===0) return;
document.getElementById('output-section').style.display='block';
var title = document.getElementById('title').value.trim() || '';
var slug = getSlug();
var date = getDateISO();
var desc = document.getElementById('desc').value.trim() || '';
var disqus = document.getElementById('disqus').value.trim() || '';
var cover = photos.length>0 ? photos[0].url : '';
// Tab 0: fields
var fields = 'Заголовок: '+title+'\n'
+'Slug: '+slug+'\n'
+'Дата: '+(document.getElementById('date').value||'')+'\n'
+'Описание: '+desc+'\n'
+'Обложка: '+cover+'\n'
+'Disqus ID: '+disqus;
document.getElementById('fields-out').textContent = fields;
// Tab 1: markdown content
var md = 'Текст поездки — напиши здесь.\n\n';
photos.forEach(function(p){
md += '!['+slug+']('+p.url+')\n\n';
}); });
md += '{{< youtube id="YOUTUBE_ID" >}}\n\n';
md += '---\n\n';
md += '📍 Локация\n';
md += '{{< rawhtml >}}\n';
md += '<div class="yandex-map-container">\n';
md += '<!-- Вставь embed-код карты из Яндекс Конструктора -->\n';
md += '</div>\n';
md += '{{< /rawhtml >}}\n\n';
md += '{{< rawhtml >}}\n{{< back-to-top >}}\n{{< /rawhtml >}}';
document.getElementById('md-out').textContent = md;
// Tab 2: URLs
var urls = photos.map(function(p,i){return '#'+(i+1)+': '+p.url;}).join('\n');
document.getElementById('urls-out').textContent = urls;
} }
function updateHist(){ // ---- Tabs ----
document.getElementById('hist').style.display='block'; function switchTab(i){
var html = ''; var tabs=document.querySelectorAll('.out-tab');
for(var i=0;i<urls.length;i++){ var panels=document.querySelectorAll('.out-panel');
html += '<div class="hi" data-url="'+urls[i]+'">'+urls[i]+'</div>'; tabs.forEach(function(t,j){t.className='out-tab'+(j===i?' active':'');});
} panels.forEach(function(p,j){p.className='out-panel'+(j===i?' active':'');});
document.getElementById('hl').innerHTML = html; }
var items = document.getElementById('hl').querySelectorAll('.hi');
for(var j=0;j<items.length;j++){ // ---- Copy ----
items[j].onclick = (function(u){ return function(){ navigator.clipboard.writeText(u); this.textContent='Скопировано!'; }; })(items[j].getAttribute('data-url')); function copyPanel(id, btn){
} var text = document.getElementById(id).textContent;
navigator.clipboard.writeText(text).then(function(){
var b = document.querySelector('[onclick*="'+id+'"]');
var orig = b.textContent; b.textContent='✓ Скопировано'; b.className='copy-btn ok';
setTimeout(function(){b.textContent=orig;b.className='copy-btn';},2000);
});
} }
</script> </script>
</body> </body>
</html>""" </html>
"""
@app.route('/upload/') @app.route('/upload/')
@@ -174,6 +369,8 @@ def upload_file():
return jsonify({'error': 'Нет файла'}), 400 return jsonify({'error': 'Нет файла'}), 400
f = request.files['file'] f = request.files['file']
custom_filename = request.form.get('filename', '').strip()
if not f.filename: if not f.filename:
return jsonify({'error': 'Пустое имя файла'}), 400 return jsonify({'error': 'Пустое имя файла'}), 400
@@ -181,22 +378,27 @@ def upload_file():
if ext not in ALLOWED: if ext not in ALLOWED:
return jsonify({'error': 'Формат .{} не поддерживается'.format(ext)}), 400 return jsonify({'error': 'Формат .{} не поддерживается'.format(ext)}), 400
original_name = os.path.splitext(f.filename)[0] # Use custom filename from wizard if provided
if custom_filename:
# HEIC/HEIF → конвертируем в JPG out_name = custom_filename
is_heic = ext in ('heic', 'heif') out_ext = out_name.rsplit('.', 1)[-1].lower()
out_ext = 'jpg' if is_heic else ext else:
s3_key = '{}{}.{}'.format(PREFIX, original_name, out_ext) original = os.path.splitext(f.filename)[0]
out_ext = 'jpg' if ext in ('heic', 'heif') else ext
out_name = '{}.{}'.format(original, out_ext)
s3_key = 'images/{}'.format(out_name)
tmp_path = None tmp_path = None
try: try:
with tempfile.NamedTemporaryFile(delete=False, suffix='.{}'.format(ext)) as tmp: with tempfile.NamedTemporaryFile(delete=False, suffix='.{}'.format(ext)) as tmp:
f.save(tmp.name) f.save(tmp.name)
tmp_path = tmp.name tmp_path = tmp.name
if is_heic: # HEIC → JPG conversion
if ext in ('heic', 'heif'):
img = Image.open(tmp_path) img = Image.open(tmp_path)
converted = tmp_path.replace('.{}'.format(ext), '.jpg') converted = tmp_path.rsplit('.', 1)[0] + '.jpg'
img.convert('RGB').save(converted, 'JPEG', quality=92) img.convert('RGB').save(converted, 'JPEG', quality=92)
os.unlink(tmp_path) os.unlink(tmp_path)
tmp_path = converted tmp_path = converted
@@ -208,14 +410,13 @@ def upload_file():
s3.upload_file( s3.upload_file(
tmp_path, BUCKET, s3_key, tmp_path, BUCKET, s3_key,
ExtraArgs={ ExtraArgs={
'ContentType': MIME.get(ext, 'application/octet-stream'), 'ContentType': MIME.get(out_ext, 'application/octet-stream'),
'CacheControl': 'max-age=31536000' 'CacheControl': 'max-age=31536000'
} }
) )
url = 'https://s3.regru.cloud/{}/{}'.format(BUCKET, s3_key) url = 'https://s3.regru.cloud/{}/{}'.format(BUCKET, s3_key)
# hint: HEIC was converted to JPG return jsonify({'url': url, 'filename': out_name})
return jsonify({'url': url})
except Exception as e: except Exception as e:
return jsonify({'error': str(e)}), 500 return jsonify({'error': str(e)}), 500