mirror of
https://github.com/studiokaiji/nostr-webhost.git
synced 2025-12-17 23:04:23 +01:00
Merge pull request #56 from studiokaiji/feature-#46-nip95-file-upload
Feature #46 nip95 file upload
This commit is contained in:
3
example/public/test.json
Normal file
3
example/public/test.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "test"
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/consts"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/keystore"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/relays"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
@@ -73,6 +72,19 @@ func Deploy(basePath string, replaceable bool, htmlIdentifier string) (string, s
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
// basePath以下のText Fileのパスをすべて羅列する
|
||||
err = generateEventsAndAddQueueAllValidStaticTextFiles(
|
||||
priKey,
|
||||
pubKey,
|
||||
htmlIdentifier,
|
||||
basePath,
|
||||
replaceable,
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Failed to convert text files:", err)
|
||||
return "", "", "", err
|
||||
}
|
||||
|
||||
// basePath以下のMedia Fileのパスを全て羅列しアップロード
|
||||
err = uploadAllValidStaticMediaFiles(priKey, pubKey, basePath)
|
||||
if err != nil {
|
||||
@@ -107,8 +119,8 @@ func Deploy(basePath string, replaceable bool, htmlIdentifier string) (string, s
|
||||
fmt.Println("❌ Failed to get public key:", err)
|
||||
return "", "", "", err
|
||||
}
|
||||
addNostrEventQueue(event)
|
||||
fmt.Println("Added", filePath, "event to publish queue")
|
||||
|
||||
addNostrEventQueue(event, filePath)
|
||||
|
||||
eventId, encoded := publishEventsFromQueue(replaceable)
|
||||
|
||||
@@ -157,7 +169,7 @@ func convertLinks(
|
||||
// jsファイルを解析する
|
||||
if strings.HasSuffix(a.Val, ".js") {
|
||||
// アップロード済みファイルの元パスとURLを取得
|
||||
for path, url := range uploadedMediaFiles {
|
||||
for path, url := range uploadedMediaFilePathToURL {
|
||||
// JS内に該当ファイルがあったら置換
|
||||
content = strings.ReplaceAll(content, path, url)
|
||||
}
|
||||
@@ -169,8 +181,7 @@ func convertLinks(
|
||||
break
|
||||
}
|
||||
|
||||
addNostrEventQueue(event)
|
||||
fmt.Println("Added", filePath, "event to publish queue")
|
||||
addNostrEventQueue(event, filePath)
|
||||
|
||||
// 置き換え可能なイベントでない場合
|
||||
if !replaceable {
|
||||
@@ -184,26 +195,6 @@ func convertLinks(
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +203,3 @@ func convertLinks(
|
||||
convertLinks(priKey, pubKey, basePath, replaceable, indexHtmlIdentifier, c)
|
||||
}
|
||||
}
|
||||
|
||||
func convertLinksFromJS() {
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -17,22 +16,6 @@ import (
|
||||
"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",
|
||||
@@ -49,24 +32,6 @@ var availableContentSuffixes = []string{
|
||||
".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 {
|
||||
@@ -82,21 +47,21 @@ type MediaResult struct {
|
||||
}
|
||||
|
||||
// [元パス]:[URL]の形で記録する
|
||||
var uploadedMediaFiles = map[string]string{}
|
||||
var uploadedMediaFilePathToURL = map[string]string{}
|
||||
|
||||
func uploadMediaFiles(filePaths []string, requests []*http.Request) {
|
||||
fmt.Println("Uploading media files...")
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
var uploadedMediaFilesCount = 0
|
||||
var uploadedMediaFilePathToURLCount = 0
|
||||
var allMediaFilesCount = len(requests)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tools.DisplayProgressBar(&uploadedMediaFilesCount, &allMediaFilesCount)
|
||||
tools.DisplayProgressBar(&uploadedMediaFilePathToURLCount, &allMediaFilesCount)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
@@ -140,9 +105,9 @@ func uploadMediaFiles(filePaths []string, requests []*http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedMediaFilesCount++ // カウントアップ
|
||||
uploadedMediaFiles[filePath] = result.Url
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedMediaFilePathToURLCount++ // カウントアップ
|
||||
uploadedMediaFilePathToURL[filePath] = result.Url
|
||||
mutex.Unlock() // ロック解除
|
||||
}(filePath, req)
|
||||
}
|
||||
@@ -152,7 +117,7 @@ func uploadMediaFiles(filePaths []string, requests []*http.Request) {
|
||||
|
||||
func filePathToUploadMediaRequest(basePath, filePath, priKey, pubKey string) (*http.Request, error) {
|
||||
// ファイルを開く
|
||||
file, err := os.Open(filepath.Join(basePath, filePath))
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to read %s: %w", filePath, err)
|
||||
}
|
||||
@@ -221,39 +186,7 @@ func filePathToUploadMediaRequest(basePath, filePath, priKey, pubKey string) (*h
|
||||
|
||||
// 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
|
||||
return tools.FindFilesWithBasePathBySuffixes(basePath, availableContentSuffixes)
|
||||
}
|
||||
|
||||
// basePath以下のMedia Fileのパスを全て羅列しアップロード
|
||||
|
||||
@@ -53,13 +53,13 @@ func publishEventsFromQueue(replaceable bool) (string, string) {
|
||||
|
||||
// Publishの進捗状況を表示
|
||||
allEventsCount := len(nostrEventsQueue)
|
||||
uploadedMediaFilesCount := 0
|
||||
uploadedMediaFilePathToURLCount := 0
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
tools.DisplayProgressBar(&uploadedMediaFilesCount, &allEventsCount)
|
||||
tools.DisplayProgressBar(&uploadedMediaFilePathToURLCount, &allEventsCount)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
@@ -76,17 +76,17 @@ func publishEventsFromQueue(replaceable bool) (string, string) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedMediaFilesCount++ // カウントアップ
|
||||
mutex.Unlock() // ロック解除
|
||||
wg.Done() // ゴルーチンの終了を通知
|
||||
mutex.Lock() // ロックして排他制御
|
||||
uploadedMediaFilePathToURLCount++ // カウントアップ
|
||||
mutex.Unlock() // ロック解除
|
||||
wg.Done() // ゴルーチンの終了を通知
|
||||
}(ev)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if uploadedMediaFilesCount < allEventsCount {
|
||||
fmt.Println("Failed to deploy", allEventsCount-uploadedMediaFilesCount, "files.")
|
||||
if uploadedMediaFilePathToURLCount < allEventsCount {
|
||||
fmt.Println("Failed to deploy", allEventsCount-uploadedMediaFilePathToURLCount, "files.")
|
||||
}
|
||||
|
||||
indexEvent := nostrEventsQueue[len(nostrEventsQueue)-1]
|
||||
@@ -140,6 +140,7 @@ func getReplaceableIdentifier(indexHtmlIdentifier, filePath string) string {
|
||||
|
||||
var nostrEventsQueue []*nostr.Event
|
||||
|
||||
func addNostrEventQueue(event *nostr.Event) {
|
||||
func addNostrEventQueue(event *nostr.Event, filePath string) {
|
||||
nostrEventsQueue = append(nostrEventsQueue, event)
|
||||
fmt.Println("Added", filePath, "event to publish queue")
|
||||
}
|
||||
|
||||
90
hostr/cmd/deploy/textFile.go
Normal file
90
hostr/cmd/deploy/textFile.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/nbd-wtf/go-nostr"
|
||||
"github.com/studiokaiji/nostr-webhost/hostr/cmd/tools"
|
||||
)
|
||||
|
||||
// 有効なText Fileの拡張子
|
||||
var availableTextFileSuffixes = []string{
|
||||
".txt",
|
||||
".csv",
|
||||
".pdf",
|
||||
".json",
|
||||
".yml",
|
||||
".yaml",
|
||||
".svg",
|
||||
}
|
||||
|
||||
// [Text Fileの拡張子]: Content-Typeで記録する
|
||||
var availableTextFileContentTypes = map[string]string{
|
||||
".txt": "text/plain",
|
||||
".csv": "text/csv",
|
||||
".pdf": "application/pdf",
|
||||
".json": "application/json",
|
||||
".yml": "application/x-yaml",
|
||||
".yaml": "application/x-yaml",
|
||||
".svg": "image/svg+xml",
|
||||
}
|
||||
|
||||
// [元パス]:[event]の形で記録する
|
||||
var textFilePathToEvent = map[string]*nostr.Event{}
|
||||
|
||||
// basePath以下のText Fileのパスを全て羅列する
|
||||
func listAllValidStaticTextFiles(basePath string) ([]string, error) {
|
||||
return tools.FindFilesWithBasePathBySuffixes(basePath, availableTextFileSuffixes)
|
||||
}
|
||||
|
||||
// Text fileをBase pathから割り出して、eventを生成しキューに追加
|
||||
func generateEventsAndAddQueueAllValidStaticTextFiles(priKey, pubKey, indexHtmlIdentifier, basePath string, replaceable bool) error {
|
||||
filePaths, err := listAllValidStaticTextFiles(basePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, filePath := range filePaths {
|
||||
// ファイルを開く
|
||||
bytesContent, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// ファイル内容をbase64エンコード
|
||||
content := base64.StdEncoding.EncodeToString(bytesContent)
|
||||
|
||||
tags := nostr.Tags{}
|
||||
// 置き換え可能なイベントの場合
|
||||
if replaceable {
|
||||
fileIdentifier := getReplaceableIdentifier(indexHtmlIdentifier, filePath)
|
||||
tags = tags.AppendUnique(nostr.Tag{"d", fileIdentifier})
|
||||
}
|
||||
|
||||
// 拡張子からContent-Typeを取得
|
||||
contentType := availableTextFileContentTypes[filepath.Ext(filePath)]
|
||||
tags = tags.AppendUnique(nostr.Tag{"type", contentType})
|
||||
|
||||
// kindを設定
|
||||
var kind int
|
||||
if replaceable {
|
||||
kind = 30064
|
||||
} else {
|
||||
kind = 1064
|
||||
}
|
||||
|
||||
// eventを取得
|
||||
event, err := getEvent(priKey, pubKey, content, kind, tags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
textFilePathToEvent[filePath] = event
|
||||
|
||||
addNostrEventQueue(event, filePath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
44
hostr/cmd/tools/findFilePaths.go
Normal file
44
hostr/cmd/tools/findFilePaths.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 特定のパス以下のファイルを検索し、与えられたsuffixesに該当するファイルのパスのみを返す
|
||||
func FindFilesWithBasePathBySuffixes(basePath string, suffixes []string) ([]string, error) {
|
||||
filePaths := []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 suffixes {
|
||||
// ファイル名とサフィックスがマッチした場合
|
||||
if strings.HasSuffix(strings.ToLower(info.Name()), strings.ToLower(suffix)) {
|
||||
// フルパスからbasePathまでの相対パスを計算
|
||||
if err != nil {
|
||||
fmt.Println("❌ Error calculating relative path:", err)
|
||||
continue
|
||||
}
|
||||
// マッチするファイルの相対パスをスライスに追加
|
||||
filePaths = append(filePaths, path)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return filePaths, nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ require (
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/nbd-wtf/go-nostr v0.20.0
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136
|
||||
golang.org/x/net v0.14.0
|
||||
golang.org/x/term v0.11.0
|
||||
)
|
||||
@@ -53,7 +54,6 @@ require (
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
golang.org/x/arch v0.4.0 // indirect
|
||||
golang.org/x/crypto v0.12.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
|
||||
Reference in New Issue
Block a user