FEAT: add basic function to save image to S3

This commit is contained in:
Mr.Chip
2024-12-09 13:21:37 +08:00
parent c614300351
commit 0d353d7ddd
7 changed files with 389 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
from .nodes.images import SaveImageS3
from .server import *
NODE_CLASS_MAPPINGS = {
"SaveImageS3": SaveImageS3
}
WEB_DIRECTORY = "./web"
+62
View File
@@ -0,0 +1,62 @@
from minio import Minio
from minio.error import S3Error
class Settings:
def __init__(self):
self.url = None
self.bucket = None
self.access_key = None
self.secret_key = None
class S3Client(object):
def __init__(self, url, access_key, secret_key, bucket):
self._url = url
self._access_key = access_key
self._secret_key = secret_key
self._bucket = bucket
self.client = Minio(
endpoint=self._url,
access_key=self._access_key,
secret_key=self._secret_key,
secure=False)
def upload_image(self, object_name, data, metadata=None):
try:
self.client.put_object(
bucket_name=self._bucket,
object_name=object_name,
data=data,
length=data.getbuffer().nbytes,
content_type='image/png',
metadata=metadata
)
except S3Error as err:
print(err)
def upload_meta(self, object_name, data):
try:
self.client.put_object(
bucket_name=self._bucket,
object_name=object_name,
data=data,
length=data.getbuffer().nbytes,
content_type='application/json',
)
except S3Error as err:
print(err)
g_settings = Settings()
g_s3_client = None
def clear_s3_client():
global g_s3_client
g_s3_client = None
def get_s3_client():
global g_s3_client
global g_settings
if g_s3_client is None:
g_s3_client = S3Client(g_settings.url, g_settings.access_key, g_settings.secret_key, g_settings.bucket)
return g_s3_client
+1
View File
@@ -0,0 +1 @@
# intentionally left blank
+83
View File
@@ -0,0 +1,83 @@
from nodes import SaveImage
import importlib
import numpy as np
from comfy.cli_args import args
import os
from io import BytesIO
from PIL import Image, ImageOps
from PIL.ExifTags import TAGS, GPSTAGS, IFD
from PIL.PngImagePlugin import PngImageFile, PngInfo
from PIL.JpegImagePlugin import JpegImageFile
import json
import folder_paths
from ..common import get_s3_client, S3Client
class SaveImageS3(SaveImage):
def __init__(self):
super().__init__()
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "The images to save."}),
"filename_prefix": ("STRING", {"default": "ComfyUI", "tooltip": "The prefix for the file to save. This may include formatting information such as %date:yyyy-MM-dd% or %Empty Latent Image.width% to include values from nodes."})
},
"hidden": {
"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"
},
}
RETURN_TYPES = ()
FUNCTION = "save_images"
OUTPUT_NODE = True
CATEGORY = "image"
DESCRIPTION = "Saves the input images to your S3 storage."
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None):
s3_client = get_s3_client()
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
for (batch_number, image) in enumerate(images):
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not args.disable_metadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace("%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
# save meta info in json file
meta_file = f"{filename_with_batch_num}_{counter:05}_.json"
oss_meta = {}
oss_meta["prompt"] = prompt
if extra_pnginfo is not None:
for x in extra_pnginfo:
oss_meta[x] = extra_pnginfo[x]
data = BytesIO()
img.save(data, format="PNG", pnginfo=metadata, compress_level=self.compress_level)
data.seek(0)
oss_meta_json = json.dumps(oss_meta, indent=4).encode('utf-8')
s3_client.upload_image(file, data)
s3_client.upload_meta(meta_file, BytesIO(oss_meta_json))
img.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
return { "ui": { "images": results } }
+1
View File
@@ -0,0 +1 @@
from .setting import *
+100
View File
@@ -0,0 +1,100 @@
from server import PromptServer
from aiohttp import web
from ..common import g_settings, clear_s3_client
@PromptServer.instance.routes.patch("/saveimages3/setting")
async def new_setting(request):
try:
settings = await request.json()
if 'url' in settings is not None and 'bucket' in settings is not None \
and 'accessKey' in settings is not None and'secretKey' in settings is not None:
url = settings['url']
bucket = settings['bucket']
accessKey = settings['access']
secretKey = settings['secret']
if type(url) is not str:
raise Exception('url must be string')
if type(bucket) is not str:
raise Exception('bucket must be string')
if type(accessKey) is not str:
raise Exception('accessKey must be string')
if type(secretKey) is not str:
raise Exception('secretKey must be string')
g_settings.url = url
g_settings.access_key = accessKey
g_settings.secret_key = secretKey
g_settings.bucket = bucket
clear_s3_client()
return web.Response(status=200)
except Exception as e:
return web.Response(status=400, text=str(e))
@PromptServer.instance.routes.patch("/saveimages3/setting/url")
async def update_url(request):
try:
settings = await request.json()
if 'url' in settings is not None:
url = settings['url']
if type(url) is not str:
raise Exception('url must be string')
g_settings.url = url
clear_s3_client()
return web.Response(status=200)
except Exception as e:
return web.Response(status=400, text=str(e))
@PromptServer.instance.routes.patch("/saveimages3/setting/bucket")
async def update_bucket(request):
try:
settings = await request.json()
if 'bucket' in settings is not None:
bucket = settings['bucket']
if type(bucket) is not str:
raise Exception('bucket must be string')
g_settings.bucket = bucket
clear_s3_client()
return web.Response(status=200)
except Exception as e:
return web.Response(status=400, text=str(e))
@PromptServer.instance.routes.patch("/saveimages3/setting/access")
async def update_access(request):
try:
settings = await request.json()
if 'access' in settings is not None:
accessKey = settings['access']
if type(accessKey) is not str:
raise Exception('bucket must be string')
g_settings.access_key = accessKey
clear_s3_client()
return web.Response(status=200)
except Exception as e:
return web.Response(status=400, text=str(e))
@PromptServer.instance.routes.patch("/saveimages3/setting/secret")
async def update_secret(request):
try:
settings = await request.json()
if 'secret' in settings is not None:
secretKey = settings['secret']
if type(secretKey) is not str:
raise Exception('bucket must be string')
g_settings.secret_key = secretKey
clear_s3_client()
return web.Response(status=200)
except Exception as e:
return web.Response(status=400, text=str(e))
+133
View File
@@ -0,0 +1,133 @@
import { ComfyApp, app } from "../../scripts/app.js"
import { api } from "../../scripts/api.js";
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
const debounce_update_url_settings = debounce(update_settings, 1000);
const debounce_update_bucket_settings = debounce(update_settings, 1000);
const debounce_update_access_settings = debounce(update_settings, 1000);
const debounce_update_secret_settings = debounce(update_settings, 1000);
async function update_settings(setting, value) {
console.log(`Setting ${setting}: ${value}`);
const data = {}
data[setting] = value;
const resp = await api.fetchApi(`/saveimages3/setting/${setting}`, {
method: 'PATCH',
body: JSON.stringify(data),
cache: 'no-store',
});
if (resp.status === 200) {
return await resp.text();
}
throw new Error(resp.statusText);
}
class SaveImageS3Plugin
{
extentionName = "Chipset.SaveImageS3";
createSettingsURL = () => {
this.settingsURL = {
id: "Chipset.SaveImageS3.URL",
type: "string",
name: "S3 URL",
category: ['SaveImageS3', 'Configuration', 'url'],
tooltip: 'S3 URL',
type: "text",
defaultValue: "",
onChange: (value) => {
debounce_update_url_settings("url", value);
}
}
}
createSettingsAccessKey = () => {
this.settingsAccessKey = {
id: plugin.extentionName + ".AccessKey",
type: "string",
name: "Access Key",
category: ['SaveImageS3', 'Configuration', 'access'],
tooltip: 'Access Key',
type: "text",
defaultValue: "",
onChange: (value) => {
debounce_update_access_settings('access', value);
}
}
}
createSettingsSecretKey = () => {
this.settingsSecretKey = {
id: plugin.extentionName + ".SecretKey",
type: "string",
name: "Secret Key",
category: ['SaveImageS3', 'Configuration', 'secret'],
tooltip: 'Secret Key',
type: "text",
defaultValue: "",
onChange: (value) => {
debounce_update_secret_settings('secret', value);
}
}
}
createSettingsBucketName = () => {
this.settingsBucketName = {
id: plugin.extentionName + ".BucketName",
type: "string",
name: "Bucket Name",
category: ['SaveImageS3', 'Configuration', 'bucket'],
tooltip: 'Bucket Name',
type: "text",
defaultValue: "",
onChange: (value) => {
debounce_update_bucket_settings('bucket', value);
}
}
}
addSettingsUI = () => {
app.ui.settings.addSetting(this.settingsSecretKey);
app.ui.settings.addSetting(this.settingsAccessKey);
app.ui.settings.addSetting(this.settingsBucketName);
app.ui.settings.addSetting(this.settingsURL);
}
}
const plugin = new SaveImageS3Plugin();
app.registerExtension({
name: plugin.extentionName,
async setup() {
console.log("Comfy.SaveImageS3 setup!!!");
plugin.createSettingsURL();
plugin.createSettingsAccessKey();
plugin.createSettingsSecretKey();
plugin.createSettingsBucketName();
plugin.addSettingsUI();
},
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// console.log(nodeType.comfyClass);
if (nodeType.comfyClass=="SaveImageS3") {
console.log("SaveImageS3 node is registed")
}
}
})