mirror of
https://github.com/studiokaiji/nostr-webhost.git
synced 2026-01-31 20:54:49 +01:00
Merge pull request #48 from studiokaiji/feature-#8-image-upload
Feature #8 image upload
This commit is contained in:
BIN
example/public/DALL·E Ostrich Hero.png
Normal file
BIN
example/public/DALL·E Ostrich Hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 983 KiB |
BIN
example/public/DALL·E Ostrich Illustration (1).png
Normal file
BIN
example/public/DALL·E Ostrich Illustration (1).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 MiB |
BIN
example/public/DALL·E Ostrich Illustration (2).png
Normal file
BIN
example/public/DALL·E Ostrich Illustration (2).png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
example/public/DALL·E Ostrich Illustration.png
Normal file
BIN
example/public/DALL·E Ostrich Illustration.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
BIN
example/public/DALL·E Ostrich on Jamaican Beach.png
Normal file
BIN
example/public/DALL·E Ostrich on Jamaican Beach.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
@@ -1,41 +1,15 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { H1 } from "./components/H1";
|
||||
import { Link } from "./components/Link";
|
||||
|
||||
function App() {
|
||||
const htmlId = useMemo(() => window.location.pathname.split("/")[2], []);
|
||||
const cssId = useMemo(
|
||||
() => Array.from(document.styleSheets)[0].href?.split("/").slice(-1)[0],
|
||||
[]
|
||||
);
|
||||
const jsId = useMemo(
|
||||
() => Array.from(document.scripts)[0].src?.split("/").slice(-1)[0],
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(Array.from(document.styleSheets));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="font-body font-medium">
|
||||
<div className="h-screen relative w-full bg-primary text-white space-y-7 p-7">
|
||||
<div className="font-body font-medium p-7">
|
||||
<div className="h-screen relative w-full text-white space-y-7">
|
||||
<H1 className="!leading-[0.8]">
|
||||
Nostr
|
||||
<br />
|
||||
WebHost
|
||||
</H1>
|
||||
<div>
|
||||
<p>
|
||||
Relay URL:{" "}
|
||||
<Link href="wss://hostr.cc">
|
||||
wss://hostr.cc
|
||||
</Link>
|
||||
</p>
|
||||
<p>HTML ID: {htmlId}</p>
|
||||
<p>CSS ID: {cssId}</p>
|
||||
<p>Javascript ID: {jsId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
GitHub:{" "}
|
||||
@@ -56,10 +30,36 @@ function App() {
|
||||
https://github.com/studiokaiji
|
||||
</Link>
|
||||
</p>
|
||||
<p>Lightning Address: floppystore07@walletofsatoshi.com</p>
|
||||
</div>
|
||||
|
||||
<div className="text-6xl font-extrabold bg-rainbow-gradient bg-clip-text text-transparent">
|
||||
↓↓ CUTE OSTRICHES GALLERY ↓↓
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap">
|
||||
<img
|
||||
src="/DALL·E Ostrich on Jamaican Beach.png"
|
||||
width={512}
|
||||
height={512}
|
||||
/>
|
||||
<img src="/DALL·E Ostrich Hero.png" width={512} height={512} />
|
||||
<img
|
||||
src="/DALL·E Ostrich Illustration (1).png"
|
||||
width={512}
|
||||
height={512}
|
||||
/>
|
||||
<img
|
||||
src="/DALL·E Ostrich Illustration (2).png"
|
||||
width={512}
|
||||
height={512}
|
||||
/>
|
||||
<img
|
||||
src="/DALL·E Ostrich Illustration.png"
|
||||
width={512}
|
||||
height={512}
|
||||
/>
|
||||
</div>
|
||||
<p className="absolute right-7 bottom-7">
|
||||
😩{"<"}Hmmm... I want to ride an ostrich...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
body {
|
||||
cursor: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMjAiIGZpbGw9IiM4RTMwRUIiLz4KPC9zdmc+Cg==, auto);
|
||||
height: 100vh;
|
||||
@apply bg-primary;
|
||||
}
|
||||
@@ -9,6 +9,10 @@ export default {
|
||||
colors: {
|
||||
primary: "#8e30eb",
|
||||
},
|
||||
backgroundImage: {
|
||||
"rainbow-gradient":
|
||||
"linear-gradient(to right,#e60000,#f39800,#fff100,#009944,#0068b7,#1d2088,#920783,#e60000)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
@@ -3,144 +3,26 @@ package deploy
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/consts"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/keystore"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/relays"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/tools"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
func pathToKind(path string, replaceable bool) (int, error) {
|
||||
// パスを分割
|
||||
separatedPath := strings.Split(path, ".")
|
||||
// 拡張子を取得
|
||||
ex := separatedPath[len(separatedPath)-1]
|
||||
// replaceable(NIP-33)の場合はReplaceableなkindを返す
|
||||
switch ex {
|
||||
case "html":
|
||||
if replaceable {
|
||||
return consts.KindWebhostHTML, nil
|
||||
} else {
|
||||
return consts.KindWebhostReplaceableHTML, nil
|
||||
}
|
||||
case "css":
|
||||
if replaceable {
|
||||
return consts.KindWebhostReplaceableCSS, nil
|
||||
} else {
|
||||
return consts.KindWebhostCSS, nil
|
||||
}
|
||||
case "js":
|
||||
if replaceable {
|
||||
return consts.KindWebhostReplaceableJS, nil
|
||||
} else {
|
||||
return consts.KindWebhostJS, nil
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("Invalid path")
|
||||
}
|
||||
}
|
||||
|
||||
// Replaceableにする場合のidentifier(dタグ)を取得
|
||||
func getReplaceableIdentifier(indexHtmlIdentifier, filePath string) string {
|
||||
return indexHtmlIdentifier + "/" + filePath[1:]
|
||||
}
|
||||
|
||||
var nostrEventsQueue []*nostr.Event
|
||||
|
||||
func addNostrEventQueue(event *nostr.Event) {
|
||||
nostrEventsQueue = append(nostrEventsQueue, event)
|
||||
}
|
||||
|
||||
var allRelays []string
|
||||
|
||||
func publishEventsFromQueue(replaceable bool) (string, string) {
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("Publishing...")
|
||||
|
||||
// 各リレーに接続
|
||||
var relays []*nostr.Relay
|
||||
|
||||
for _, url := range allRelays {
|
||||
relay, err := nostr.RelayConnect(ctx, url)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to connect to:", url)
|
||||
continue
|
||||
}
|
||||
relays = append(relays, relay)
|
||||
}
|
||||
|
||||
// Publishの進捗状況を表示
|
||||
allEventsCount := len(nostrEventsQueue)
|
||||
uploadedFilesCount := 0
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tools.DisplayProgressBar(&uploadedFilesCount, &allEventsCount)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
// リレーへpublish
|
||||
for _, ev := range nostrEventsQueue {
|
||||
wg.Add(1)
|
||||
go func(event *nostr.Event) {
|
||||
for _, relay := range relays {
|
||||
_, err := relay.Publish(ctx, *event)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedFilesCount++ // カウントアップ
|
||||
mutex.Unlock() // ロック解除
|
||||
wg.Done() // ゴルーチンの終了を通知
|
||||
}(ev)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if uploadedFilesCount < allEventsCount {
|
||||
fmt.Println("Failed to deploy", allEventsCount-uploadedFilesCount, "files.")
|
||||
}
|
||||
|
||||
indexEvent := nostrEventsQueue[len(nostrEventsQueue)-1]
|
||||
|
||||
encoded := ""
|
||||
if !replaceable {
|
||||
if enc, err := nip19.EncodeEvent(indexEvent.ID, allRelays, indexEvent.PubKey); err == nil {
|
||||
encoded = enc
|
||||
} else {
|
||||
fmt.Println("❌ Failed to covert nevent:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return indexEvent.ID, encoded
|
||||
}
|
||||
|
||||
func isExternalURL(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
return err == nil && u.Scheme != "" && u.Host != ""
|
||||
}
|
||||
|
||||
func isValidFileType(str string) bool {
|
||||
return strings.HasSuffix(str, ".html") || strings.HasSuffix(str, ".css") || strings.HasSuffix(str, ".js")
|
||||
}
|
||||
|
||||
func Deploy(basePath string, replaceable bool, htmlIdentifier string) (string, string, error) {
|
||||
// 引数からデプロイしたいサイトのパスを受け取る。
|
||||
filePath := filepath.Join(basePath, "index.html")
|
||||
@@ -187,6 +69,15 @@ func Deploy(basePath string, replaceable bool, htmlIdentifier string) (string, s
|
||||
// リレーを取得
|
||||
allRelays, err = relays.GetAllRelays()
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to get all relays:", err)
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
|
||||
// basePath以下のMedia Fileのパスを全て羅列しアップロード
|
||||
err = uploadAllValidStaticMediaFiles(priKey, pubKey, basePath)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to upload media:", err)
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
@@ -225,56 +116,93 @@ func Deploy(basePath string, replaceable bool, htmlIdentifier string) (string, s
|
||||
return eventId, encoded, err
|
||||
}
|
||||
|
||||
func convertLinks(priKey, pubKey, basePath string, replaceable bool, indexHtmlIdentifier string, n *html.Node) {
|
||||
// <link> と <script> タグを対象とする
|
||||
if n.Type == html.ElementNode && (n.Data == "link" || n.Data == "script") {
|
||||
for i, a := range n.Attr {
|
||||
// href,srcのうち、外部URLでないものかつ. html, .css, .js のみ変換する
|
||||
if (a.Key == "href" || a.Key == "src") && !isExternalURL(a.Val) && isValidFileType(a.Val) {
|
||||
filePath := filepath.Join(basePath, a.Val)
|
||||
func convertLinks(
|
||||
priKey, pubKey, basePath string,
|
||||
replaceable bool,
|
||||
indexHtmlIdentifier string,
|
||||
n *html.Node,
|
||||
) {
|
||||
if n.Type == html.ElementNode {
|
||||
if n.Data == "link" || n.Data == "script" {
|
||||
// <link> と <script> タグを対象としてNostr Eventを作成
|
||||
for i, a := range n.Attr {
|
||||
// href,srcのうち、外部URLでないものかつ. html, .css, .js のみ変換する
|
||||
if (a.Key == "href" || a.Key == "src") && !isExternalURL(a.Val) && isValidBasicFileType(a.Val) {
|
||||
filePath := filepath.Join(basePath, a.Val)
|
||||
|
||||
// kindを取得
|
||||
kind, err := pathToKind(filePath, replaceable)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// contentを取得
|
||||
bytesContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to read", filePath, ":", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Tagsを追加
|
||||
tags := nostr.Tags{}
|
||||
// 置き換え可能なイベントの場合
|
||||
if replaceable {
|
||||
fileIdentifier := getReplaceableIdentifier(indexHtmlIdentifier, a.Val)
|
||||
tags = tags.AppendUnique(nostr.Tag{"d", fileIdentifier})
|
||||
// 元のパスをfileIdentifierに置き換える
|
||||
n.Attr[i].Val = fileIdentifier
|
||||
}
|
||||
|
||||
// Eventを生成し、キューに追加
|
||||
event, err := getEvent(priKey, pubKey, string(bytesContent), kind, tags)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to get event for", filePath, ":", err)
|
||||
break
|
||||
}
|
||||
|
||||
addNostrEventQueue(event)
|
||||
fmt.Println("Added", filePath, "event to publish queue")
|
||||
|
||||
// 置き換え可能なイベントでない場合
|
||||
if !replaceable {
|
||||
// neventを指定
|
||||
nevent, err := nip19.EncodeEvent(event.ID, allRelays, pubKey)
|
||||
// kindを取得
|
||||
kind, err := pathToKind(filePath, replaceable)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to encode event", filePath, ":", err)
|
||||
break
|
||||
}
|
||||
n.Attr[i].Val = nevent
|
||||
|
||||
// contentを取得
|
||||
bytesContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to read", filePath, ":", err)
|
||||
continue
|
||||
}
|
||||
|
||||
content := string(bytesContent)
|
||||
|
||||
// Tagsを追加
|
||||
tags := nostr.Tags{}
|
||||
// 置き換え可能なイベントの場合
|
||||
if replaceable {
|
||||
fileIdentifier := getReplaceableIdentifier(indexHtmlIdentifier, a.Val)
|
||||
tags = tags.AppendUnique(nostr.Tag{"d", fileIdentifier})
|
||||
// 元のパスをfileIdentifierに置き換える
|
||||
n.Attr[i].Val = fileIdentifier
|
||||
}
|
||||
|
||||
// jsファイルを解析する
|
||||
if strings.HasSuffix(a.Val, ".js") {
|
||||
// アップロード済みファイルの元パスとURLを取得
|
||||
for path, url := range uploadedMediaFiles {
|
||||
// JS内に該当ファイルがあったら置換
|
||||
content = strings.ReplaceAll(content, path, url)
|
||||
}
|
||||
}
|
||||
|
||||
event, err := getEvent(priKey, pubKey, content, kind, tags)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to get event for", filePath, ":", err)
|
||||
break
|
||||
}
|
||||
|
||||
addNostrEventQueue(event)
|
||||
fmt.Println("Added", filePath, "event to publish queue")
|
||||
|
||||
// 置き換え可能なイベントでない場合
|
||||
if !replaceable {
|
||||
// neventを指定
|
||||
nevent, err := nip19.EncodeEvent(event.ID, allRelays, pubKey)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to encode event", filePath, ":", err)
|
||||
break
|
||||
}
|
||||
n.Attr[i].Val = nevent
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if slices.Contains(availableMediaHtmlTags, n.Data) {
|
||||
// 内部mediaファイルを対象にUpload Requestを作成
|
||||
for _, a := range n.Attr {
|
||||
if (a.Key == "href" || a.Key == "src" || a.Key == "data") && !isExternalURL(a.Val) && isValidMediaFileType(a.Val) {
|
||||
filePath := filepath.Join(basePath, a.Val)
|
||||
|
||||
// contentを取得
|
||||
bytesContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to read", filePath, ":", err)
|
||||
continue
|
||||
}
|
||||
|
||||
content := string(bytesContent)
|
||||
|
||||
if url, ok := uploadedMediaFiles[filePath]; ok {
|
||||
content = strings.ReplaceAll(content, filePath, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -286,19 +214,6 @@ func convertLinks(priKey, pubKey, basePath string, replaceable bool, indexHtmlId
|
||||
}
|
||||
}
|
||||
|
||||
func getEvent(priKey, pubKey, content string, kind int, tags nostr.Tags) (*nostr.Event, error) {
|
||||
ev := nostr.Event{
|
||||
PubKey: pubKey,
|
||||
CreatedAt: nostr.Now(),
|
||||
Kind: kind,
|
||||
Content: content,
|
||||
Tags: tags,
|
||||
}
|
||||
func convertLinksFromJS() {
|
||||
|
||||
err := ev.Sign(priKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ev, err
|
||||
}
|
||||
|
||||
275
hostr/cmd/deploy/media.go
Normal file
275
hostr/cmd/deploy/media.go
Normal file
@@ -0,0 +1,275 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/tools"
|
||||
)
|
||||
|
||||
var availableContentTypes = []string{
|
||||
"image/png",
|
||||
"image/jpg",
|
||||
"image/jpeg",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"video/mp4",
|
||||
"video/quicktime",
|
||||
"video/mpeg",
|
||||
"video/webm",
|
||||
"audio/mpeg",
|
||||
"audio/mpg",
|
||||
"audio/mpeg3",
|
||||
"audio/mp3",
|
||||
}
|
||||
|
||||
var availableContentSuffixes = []string{
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".gif",
|
||||
".webp",
|
||||
".mp4",
|
||||
".quicktime",
|
||||
".mpeg",
|
||||
".webm",
|
||||
".mpeg",
|
||||
".mpg",
|
||||
".mpeg3",
|
||||
".mp3",
|
||||
}
|
||||
|
||||
var availableMediaHtmlTags = []string{
|
||||
"img",
|
||||
"audio",
|
||||
"video",
|
||||
"source",
|
||||
"object",
|
||||
"embed",
|
||||
}
|
||||
|
||||
func isValidMediaFileType(path string) bool {
|
||||
for _, suffix := range availableContentSuffixes {
|
||||
if strings.HasSuffix(path, suffix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const uploadEndpoint = "https://nostrcheck.me/api/v1/media"
|
||||
|
||||
type MediaResult struct {
|
||||
Result bool `json:"result,omitempty"`
|
||||
Description string `json:"description"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Id int `json:"id,omitempty"`
|
||||
Pubkey string `json:"pubkey,omitempty"`
|
||||
Url string `json:"url,omitempty"`
|
||||
Hash string `json:"hash,omitempty"`
|
||||
Magnet string `json:"magnet,omitempty"`
|
||||
Tags []string `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
// [元パス]:[URL]の形で記録する
|
||||
var uploadedMediaFiles = map[string]string{}
|
||||
|
||||
func uploadMediaFiles(filePaths []string, requests []*http.Request) {
|
||||
client := &http.Client{}
|
||||
|
||||
var uploadedMediaFilesCount = 0
|
||||
var allMediaFilesCount = len(requests)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tools.DisplayProgressBar(&uploadedMediaFilesCount, &allMediaFilesCount)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
// アップロードを並列処理
|
||||
for i, req := range requests {
|
||||
wg.Add(1)
|
||||
filePath := filePaths[i]
|
||||
|
||||
go func(filePath string, req *http.Request) {
|
||||
defer wg.Done()
|
||||
|
||||
response, err := client.Do(req)
|
||||
// リクエストを送信
|
||||
if err != nil {
|
||||
fmt.Println("\n❌ Error sending request:", filePath, err)
|
||||
return
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if !strings.HasPrefix(fmt.Sprint(response.StatusCode), "2") {
|
||||
fmt.Println("\n❌ Failed to upload:", response.StatusCode, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
var result *MediaResult
|
||||
// ResultのDecode
|
||||
err = json.NewDecoder(response.Body).Decode(&result)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("\n❌ Error decoding response:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// アップロードに失敗した場合
|
||||
if !result.Result {
|
||||
fmt.Println("\n❌ Failed to upload file:", filePath, err)
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedMediaFilesCount++ // カウントアップ
|
||||
uploadedMediaFiles[filePath] = result.Url
|
||||
mutex.Unlock() // ロック解除
|
||||
}(filePath, req)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func filePathToUploadMediaRequest(basePath, filePath, priKey, pubKey string) (*http.Request, error) {
|
||||
// ファイルを開く
|
||||
file, err := os.Open(filepath.Join(basePath, filePath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read %s: %w", filePath, err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// リクエストボディのバッファを初期化
|
||||
var requestBody bytes.Buffer
|
||||
// multipart writerを作成
|
||||
writer := multipart.NewWriter(&requestBody)
|
||||
|
||||
// mediafileフィールドを作成
|
||||
part, err := writer.CreateFormFile("mediafile", filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error creating form file: %w", err)
|
||||
}
|
||||
// ファイルの内容をpartにコピー
|
||||
_, err = io.Copy(part, file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error copying file: %w", err)
|
||||
}
|
||||
|
||||
// uploadtypeフィールドを設定
|
||||
err = writer.WriteField("uploadtype", "media")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error writing field: %w", err)
|
||||
}
|
||||
|
||||
// writerを閉じてリクエストボディを完成させる
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error closing writer: %w", err)
|
||||
}
|
||||
|
||||
// タグを追加
|
||||
tags := nostr.Tags{
|
||||
nostr.Tag{"u", uploadEndpoint},
|
||||
nostr.Tag{"method", "POST"},
|
||||
nostr.Tag{"payload", ""},
|
||||
}
|
||||
|
||||
// イベントを生成
|
||||
ev, err := getEvent(priKey, pubKey, "", 27235, tags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error get event: %d", err)
|
||||
}
|
||||
|
||||
// イベントをJSONにマーシャル
|
||||
evJson, err := ev.MarshalJSON()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error marshaling event: %d", err)
|
||||
}
|
||||
|
||||
// HTTPリクエストを作成
|
||||
request, err := http.NewRequest("POST", uploadEndpoint, &requestBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error creating request: %d", err)
|
||||
}
|
||||
|
||||
// ヘッダーを設定
|
||||
request.Header.Set("Authorization", "Nostr "+base64.StdEncoding.EncodeToString(evJson))
|
||||
request.Header.Set("Accept", "application/json")
|
||||
request.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// basePath以下のMedia Fileのパスを全て羅列する
|
||||
func listAllValidStaticMediaFilePaths(basePath string) ([]string, error) {
|
||||
mediaFilePaths := []string{}
|
||||
|
||||
err := filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ディレクトリはスキップ
|
||||
if !info.IsDir() {
|
||||
// 各サフィックスに対してマッチングを試みる
|
||||
for _, suffix := range availableContentSuffixes {
|
||||
// ファイル名とサフィックスがマッチした場合
|
||||
if strings.HasSuffix(strings.ToLower(info.Name()), strings.ToLower(suffix)) {
|
||||
// フルパスからbasePathまでの相対パスを計算
|
||||
relPath, err := filepath.Rel(basePath, path)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Error calculating relative path:", err)
|
||||
continue
|
||||
}
|
||||
// マッチするファイルの相対パスをスライスに追加
|
||||
mediaFilePaths = append(mediaFilePaths, "/"+relPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return mediaFilePaths, nil
|
||||
}
|
||||
|
||||
// basePath以下のMedia Fileのパスを全て羅列しアップロード
|
||||
func uploadAllValidStaticMediaFiles(priKey, pubKey, basePath string) error {
|
||||
filesPaths, err := listAllValidStaticMediaFilePaths(basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requests := []*http.Request{}
|
||||
|
||||
for _, filePath := range filesPaths {
|
||||
request, err := filePathToUploadMediaRequest(basePath, filePath, priKey, pubKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requests = append(requests, request)
|
||||
}
|
||||
|
||||
uploadMediaFiles(filesPaths, requests)
|
||||
|
||||
return nil
|
||||
}
|
||||
145
hostr/cmd/deploy/nostr.go
Normal file
145
hostr/cmd/deploy/nostr.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/nbd-wtf/go-nostr/nip19"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/consts"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/tools"
|
||||
)
|
||||
|
||||
var allRelays []string
|
||||
|
||||
func getEvent(priKey, pubKey, content string, kind int, tags nostr.Tags) (*nostr.Event, error) {
|
||||
ev := nostr.Event{
|
||||
PubKey: pubKey,
|
||||
CreatedAt: nostr.Now(),
|
||||
Kind: kind,
|
||||
Content: content,
|
||||
Tags: tags,
|
||||
}
|
||||
|
||||
err := ev.Sign(priKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ev, err
|
||||
}
|
||||
|
||||
func isValidBasicFileType(str string) bool {
|
||||
return strings.HasSuffix(str, ".html") || strings.HasSuffix(str, ".css") || strings.HasSuffix(str, ".js")
|
||||
}
|
||||
func publishEventsFromQueue(replaceable bool) (string, string) {
|
||||
ctx := context.Background()
|
||||
|
||||
fmt.Println("Publishing...")
|
||||
|
||||
// 各リレーに接続
|
||||
var relays []*nostr.Relay
|
||||
|
||||
for _, url := range allRelays {
|
||||
relay, err := nostr.RelayConnect(ctx, url)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to connect to:", url)
|
||||
continue
|
||||
}
|
||||
relays = append(relays, relay)
|
||||
}
|
||||
|
||||
// Publishの進捗状況を表示
|
||||
allEventsCount := len(nostrEventsQueue)
|
||||
uploadedMediaFilesCount := 0
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tools.DisplayProgressBar(&uploadedMediaFilesCount, &allEventsCount)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
var mutex sync.Mutex
|
||||
|
||||
// リレーへpublish
|
||||
for _, ev := range nostrEventsQueue {
|
||||
wg.Add(1)
|
||||
go func(event *nostr.Event) {
|
||||
for _, relay := range relays {
|
||||
_, err := relay.Publish(ctx, *event)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedMediaFilesCount++ // カウントアップ
|
||||
mutex.Unlock() // ロック解除
|
||||
wg.Done() // ゴルーチンの終了を通知
|
||||
}(ev)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if uploadedMediaFilesCount < allEventsCount {
|
||||
fmt.Println("Failed to deploy", allEventsCount-uploadedMediaFilesCount, "files.")
|
||||
}
|
||||
|
||||
indexEvent := nostrEventsQueue[len(nostrEventsQueue)-1]
|
||||
|
||||
encoded := ""
|
||||
if !replaceable {
|
||||
if enc, err := nip19.EncodeEvent(indexEvent.ID, allRelays, indexEvent.PubKey); err == nil {
|
||||
encoded = enc
|
||||
} else {
|
||||
fmt.Println("❌ Failed to covert nevent:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return indexEvent.ID, encoded
|
||||
}
|
||||
|
||||
func pathToKind(path string, replaceable bool) (int, error) {
|
||||
// パスを分割
|
||||
separatedPath := strings.Split(path, ".")
|
||||
// 拡張子を取得
|
||||
ex := separatedPath[len(separatedPath)-1]
|
||||
// replaceable(NIP-33)の場合はReplaceableなkindを返す
|
||||
switch ex {
|
||||
case "html":
|
||||
if replaceable {
|
||||
return consts.KindWebhostHTML, nil
|
||||
} else {
|
||||
return consts.KindWebhostReplaceableHTML, nil
|
||||
}
|
||||
case "css":
|
||||
if replaceable {
|
||||
return consts.KindWebhostReplaceableCSS, nil
|
||||
} else {
|
||||
return consts.KindWebhostCSS, nil
|
||||
}
|
||||
case "js":
|
||||
if replaceable {
|
||||
return consts.KindWebhostReplaceableJS, nil
|
||||
} else {
|
||||
return consts.KindWebhostJS, nil
|
||||
}
|
||||
default:
|
||||
return 0, fmt.Errorf("Invalid path")
|
||||
}
|
||||
}
|
||||
|
||||
// Replaceableにする場合のidentifier(dタグ)を取得
|
||||
func getReplaceableIdentifier(indexHtmlIdentifier, filePath string) string {
|
||||
return indexHtmlIdentifier + "/" + filePath[1:]
|
||||
}
|
||||
|
||||
var nostrEventsQueue []*nostr.Event
|
||||
|
||||
func addNostrEventQueue(event *nostr.Event) {
|
||||
nostrEventsQueue = append(nostrEventsQueue, event)
|
||||
}
|
||||
Reference in New Issue
Block a user