画像のアップロード機能を実装

This commit is contained in:
studiokaiji
2023-10-25 20:13:43 +09:00
parent 8a596892ea
commit 7e4094f56d
3 changed files with 173 additions and 107 deletions

View File

@@ -3,9 +3,7 @@ package deploy
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
@@ -71,19 +69,21 @@ 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
}
// リンクの解析と変換
convertLinks(priKey, pubKey, basePath, replaceable, htmlIdentifier, doc)
if len(mediaUploadRequestQueue) > 0 {
// メディアのアップロード
fmt.Println("📷 Uploading media files")
uploadMediaFilesFromQueue()
fmt.Println("📷 Media upload finished.")
}
// 更新されたHTML
var buf bytes.Buffer
html.Render(&buf, doc)
@@ -116,7 +116,12 @@ 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) {
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を作成
@@ -128,7 +133,7 @@ func convertLinks(priKey, pubKey, basePath string, replaceable bool, indexHtmlId
// kindを取得
kind, err := pathToKind(filePath, replaceable)
if err != nil {
continue
break
}
// contentを取得
@@ -138,10 +143,7 @@ func convertLinks(priKey, pubKey, basePath string, replaceable bool, indexHtmlId
continue
}
// jsファイルを解析する
if strings.HasSuffix(basePath, ".js") {
jsContent := string(bytesContent)
}
content := string(bytesContent)
// Tagsを追加
tags := nostr.Tags{}
@@ -153,8 +155,16 @@ func convertLinks(priKey, pubKey, basePath string, replaceable bool, indexHtmlId
n.Attr[i].Val = fileIdentifier
}
// Eventを生成し、キューに追加
event, err := getEvent(priKey, pubKey, string(bytesContent), kind, tags)
// 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
@@ -177,45 +187,22 @@ func convertLinks(priKey, pubKey, basePath string, replaceable bool, indexHtmlId
}
} else if slices.Contains(availableMediaHtmlTags, n.Data) {
// 内部mediaファイルを対象にUpload Requestを作成
for i, a := range n.Attr {
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)
// アップロードのためのHTTPリクエストを取得
request, err := filePathToUploadMediaRequest(filePath, priKey, pubKey)
// contentを取得
bytesContent, err := os.ReadFile(filePath)
if err != nil {
fmt.Println("❌ Failed generate upload request: ", err)
fmt.Println("❌ Failed to read", filePath, ":", err)
continue
}
// アップロード処理を代入
uploadFunc := func(client *http.Client) (*MediaResult, error) {
response, err := client.Do(request)
// リクエストを送信
if err != nil {
return nil, fmt.Errorf("Error sending request: %w", err)
}
defer response.Body.Close()
content := string(bytesContent)
var result *MediaResult
// ResultのDecode
err = json.NewDecoder(response.Body).Decode(result)
if err != nil {
return nil, fmt.Errorf("Error decoding response: %w", err)
}
// アップロードに失敗した場合
if !result.result {
return nil, fmt.Errorf("Failed to upload file: %w", err)
}
// URLを割り当て
n.Attr[i].Val = result.url
return result, nil
if url, ok := uploadedMediaFiles[filePath]; ok {
content = strings.ReplaceAll(content, filePath, url)
}
// Queueにアップロード処理を追加
addMediaUploadRequestFuncQueue(uploadFunc)
}
}
}

View File

@@ -3,11 +3,13 @@ package deploy
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
@@ -68,68 +70,85 @@ func isValidMediaFileType(path string) bool {
const uploadEndpoint = "https://nostrcheck.me/api/v1/media"
type MediaResult struct {
result bool
description string
status string
id int
pubkey string
url string
hash string
magnet string
tags []string
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"`
}
var mediaUploadRequestQueue []func(*http.Client) (*MediaResult, error)
// [元パス]:[URL]の形で記録する
var uploadedMediaFiles = map[string]string{}
func addNostrEventQueue(event *nostr.Event) {
nostrEventsQueue = append(nostrEventsQueue, event)
}
func uploadMediaFiles(filePaths []string, requests []*http.Request) {
client := &http.Client{}
func addMediaUploadRequestFuncQueue(reqFunc func(client *http.Client) (*MediaResult, error)) {
mediaUploadRequestQueue = append(mediaUploadRequestQueue, reqFunc)
}
var allRelays []string
func uploadMediaFilesFromQueue() {
// Publishの進捗状況を表示
allEventsCount := len(mediaUploadRequestQueue)
uploadedFilesCount := 0
var uploadedMediaFilesCount = 0
var allMediaFilesCount = len(requests)
var wg sync.WaitGroup
go func() {
wg.Add(1)
tools.DisplayProgressBar(&uploadedFilesCount, &allEventsCount)
tools.DisplayProgressBar(&uploadedMediaFilesCount, &allMediaFilesCount)
wg.Done()
}()
var mutex sync.Mutex
client := &http.Client{}
// アップロードを並列処理
for _, reqFunc := range mediaUploadRequestQueue {
for i, req := range requests {
wg.Add(1)
go func(reqFun func(*http.Client) (*MediaResult, error)) {
_, err := reqFun(client)
filePath := filePaths[i]
go func(filePath string, req *http.Request) {
defer wg.Done()
response, err := client.Do(req)
// リクエストを送信
if err != nil {
fmt.Println(err)
fmt.Println("\n❌ Error sending request:", filePath, err)
return
}
mutex.Lock() // ロックして排他制御
uploadedFilesCount++ // カウントアップ
mutex.Unlock() // ロック解除
wg.Done() // ゴルーチンの終了を通知
}(reqFunc)
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(filePath, priKey, pubKey string) (*http.Request, error) {
func filePathToUploadMediaRequest(basePath, filePath, priKey, pubKey string) (*http.Request, error) {
// ファイルを開く
file, err := os.Open(filePath)
file, err := os.Open(filepath.Join(basePath, filePath))
if err != nil {
return nil, fmt.Errorf("Failed to read %s: %w", filePath, err)
}
@@ -140,39 +159,38 @@ func filePathToUploadMediaRequest(filePath, priKey, pubKey string) (*http.Reques
// multipart writerを作成
writer := multipart.NewWriter(&requestBody)
// uploadtypeフィールドを設定
err = writer.WriteField("uploadtype", "media")
if err != nil {
return nil, fmt.Errorf("Error writing field: %w", err)
}
// 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{}
// タグを追加
tags.AppendUnique(nostr.Tag{"u", uploadEndpoint})
tags.AppendUnique(nostr.Tag{"method", "POST"})
tags.AppendUnique(nostr.Tag{"payload", ""})
tags := nostr.Tags{
nostr.Tag{"u", uploadEndpoint},
nostr.Tag{"method", "POST"},
nostr.Tag{"payload", ""},
}
// イベントを生成
ev, err := getEvent(priKey, pubKey, "", 27533, tags)
ev, err := getEvent(priKey, pubKey, "", 27235, tags)
if err != nil {
return nil, fmt.Errorf("Error get event: %d", err)
}
@@ -190,9 +208,68 @@ func filePathToUploadMediaRequest(filePath, priKey, pubKey string) (*http.Reques
}
// ヘッダーを設定
request.Header.Set("Content-Type", writer.FormDataContentType())
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
}

View File

@@ -12,6 +12,8 @@ import (
"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,
@@ -51,13 +53,13 @@ func publishEventsFromQueue(replaceable bool) (string, string) {
// Publishの進捗状況を表示
allEventsCount := len(nostrEventsQueue)
uploadedFilesCount := 0
uploadedMediaFilesCount := 0
var wg sync.WaitGroup
go func() {
wg.Add(1)
tools.DisplayProgressBar(&uploadedFilesCount, &allEventsCount)
tools.DisplayProgressBar(&uploadedMediaFilesCount, &allEventsCount)
wg.Done()
}()
@@ -74,17 +76,17 @@ func publishEventsFromQueue(replaceable bool) (string, string) {
continue
}
}
mutex.Lock() // ロックして排他制御
uploadedFilesCount++ // カウントアップ
mutex.Unlock() // ロック解除
wg.Done() // ゴルーチンの終了を通知
mutex.Lock() // ロックして排他制御
uploadedMediaFilesCount++ // カウントアップ
mutex.Unlock() // ロック解除
wg.Done() // ゴルーチンの終了を通知
}(ev)
}
wg.Wait()
if uploadedFilesCount < allEventsCount {
fmt.Println("Failed to deploy", allEventsCount-uploadedFilesCount, "files.")
if uploadedMediaFilesCount < allEventsCount {
fmt.Println("Failed to deploy", allEventsCount-uploadedMediaFilesCount, "files.")
}
indexEvent := nostrEventsQueue[len(nostrEventsQueue)-1]