Add files via upload
This commit is contained in:
parent
cc5617974d
commit
4fad092e18
8 changed files with 684 additions and 0 deletions
118
contracts/market/Market.go
Normal file
118
contracts/market/Market.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package contract
|
||||
|
||||
import (
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/std"
|
||||
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
ownerKey = "o"
|
||||
tokenKey = "t"
|
||||
nftKey = "n"
|
||||
sellPrefix = "forSale_"
|
||||
)
|
||||
|
||||
// MarketDeployData структура для _deploy (пример)
|
||||
type MarketDeployData struct {
|
||||
Admin interop.Hash160
|
||||
Token interop.Hash160
|
||||
Nft interop.Hash160
|
||||
}
|
||||
|
||||
func _deploy(data any, isUpdate bool) {
|
||||
if isUpdate {
|
||||
return
|
||||
}
|
||||
|
||||
initData := data.(MarketDeployData)
|
||||
admin := initData.Admin
|
||||
if len(admin) != 20 {
|
||||
panic("invalid admin")
|
||||
}
|
||||
|
||||
ctx := storage.GetContext()
|
||||
storage.Put(ctx, []byte(ownerKey), admin)
|
||||
storage.Put(ctx, []byte(tokenKey), initData.Token)
|
||||
storage.Put(ctx, []byte(nftKey), initData.Nft)
|
||||
}
|
||||
|
||||
// OnNEP11Payment вызывается при получении NFT
|
||||
func OnNEP11Payment(from interop.Hash160, amount int, tokenId []byte, data any) {
|
||||
if amount != 1 {
|
||||
panic("Must be non-fungible (amount=1)")
|
||||
}
|
||||
ctx := storage.GetContext()
|
||||
callingHash := runtime.GetCallingScriptHash()
|
||||
nftHash := storage.Get(ctx, []byte(nftKey)).(interop.Hash160)
|
||||
if !std.Equals(callingHash, nftHash) {
|
||||
panic("NFT not recognized")
|
||||
}
|
||||
|
||||
// Выставляем NFT на продажу, если владелец хочет
|
||||
// Цена берется из свойств NFT (fields: price)
|
||||
saleKey := append([]byte(sellPrefix), tokenId...)
|
||||
storage.Put(ctx, saleKey, []byte{1})
|
||||
}
|
||||
|
||||
// OnNEP17Payment вызывается при получении токенов (оплата за NFT)
|
||||
func OnNEP17Payment(from interop.Hash160, amount int, data any) {
|
||||
ctx := storage.GetContext()
|
||||
callingHash := runtime.GetCallingScriptHash()
|
||||
myToken := storage.Get(ctx, []byte(tokenKey)).(interop.Hash160)
|
||||
if !std.Equals(callingHash, myToken) {
|
||||
panic("invalid token")
|
||||
}
|
||||
|
||||
tokenIdBytes, ok := data.([]byte)
|
||||
if !ok {
|
||||
panic("no tokenId in data")
|
||||
}
|
||||
tokenId := string(tokenIdBytes)
|
||||
|
||||
saleKey := append([]byte(sellPrefix), tokenIdBytes...)
|
||||
if storage.Get(ctx, saleKey) == nil {
|
||||
panic("NFT not on sale")
|
||||
}
|
||||
|
||||
// Узнаем price из NFT
|
||||
nftHash := storage.Get(ctx, []byte(nftKey)).(interop.Hash160)
|
||||
props := contract.Call(nftHash, "properties", contract.ReadStates, tokenId).(map[string]any)
|
||||
// price хранится как string => нужно конвертировать
|
||||
price := std.Atoi(props["price"].(string))
|
||||
if amount < price {
|
||||
panic("insufficient payment")
|
||||
}
|
||||
|
||||
// снимаем с продажи
|
||||
storage.Delete(ctx, saleKey)
|
||||
// передаем NFT покупателю
|
||||
contract.Call(nftHash, "transfer", contract.All, from, tokenId, nil)
|
||||
}
|
||||
|
||||
// List вернет массив token_id, которые сейчас на продаже
|
||||
func List() []string {
|
||||
ctx := storage.GetContext()
|
||||
iter := storage.Find(ctx, []byte(sellPrefix), storage.KeysOnly)
|
||||
var result []string
|
||||
for iterator.Next(iter) {
|
||||
key := iterator.Key(iter).([]byte)
|
||||
tokenId := key[len(sellPrefix):]
|
||||
result = append(result, string(tokenId))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// TransferTokens позволяет владельцу контракта забрать токены с контракта.
|
||||
func TransferTokens(to interop.Hash160, amount int) {
|
||||
ctx := storage.GetContext()
|
||||
owner := storage.Get(ctx, []byte(ownerKey)).(interop.Hash160)
|
||||
if !runtime.CheckWitness(owner) {
|
||||
panic("not an owner")
|
||||
}
|
||||
myToken := storage.Get(ctx, []byte(tokenKey)).(interop.Hash160)
|
||||
contract.Call(myToken, "transfer", contract.All, runtime.GetExecutingScriptHash(), to, amount, nil)
|
||||
}
|
135
contracts/nep11/OnlyFansNFT.py
Normal file
135
contracts/nep11/OnlyFansNFT.py
Normal file
|
@ -0,0 +1,135 @@
|
|||
from typing import Any, Dict, List
|
||||
from boa3.builtin import public
|
||||
from boa3.builtin.contract import abort
|
||||
from boa3.builtin.interop.runtime import check_witness
|
||||
from boa3.builtin.interop.storage import get, put, delete, find, FindOptions
|
||||
from boa3.builtin.type import UInt160
|
||||
|
||||
TOKEN_PREFIX = b'token_'
|
||||
OWNER_PREFIX = b'owner_'
|
||||
SUPPLY_KEY = b'totalSupply'
|
||||
ADMIN_KEY = b'admin'
|
||||
|
||||
@public
|
||||
def symbol() -> str:
|
||||
return "OFNFT" # OnlyFans NFT
|
||||
|
||||
@public
|
||||
def name() -> str:
|
||||
return "Web3OnlyFansNFT"
|
||||
|
||||
@public
|
||||
def totalSupply() -> int:
|
||||
return get(SUPPLY_KEY).to_int()
|
||||
|
||||
@public
|
||||
def balanceOf(owner: UInt160) -> int:
|
||||
return len(_getTokensOf(owner))
|
||||
|
||||
@public
|
||||
def ownerOf(token_id: str) -> UInt160:
|
||||
token_data = get(TOKEN_PREFIX + token_id.encode())
|
||||
if not token_data:
|
||||
abort()
|
||||
info = _deserialize_token(token_data)
|
||||
return info['owner']
|
||||
|
||||
@public
|
||||
def tokensOf(owner: UInt160) -> List[str]:
|
||||
return _getTokensOf(owner)
|
||||
|
||||
@public
|
||||
def properties(token_id: str) -> Dict[str, Any]:
|
||||
token_data = get(TOKEN_PREFIX + token_id.encode())
|
||||
if not token_data:
|
||||
abort()
|
||||
return _deserialize_token(token_data)
|
||||
|
||||
@public
|
||||
def transfer(to: UInt160, token_id: str, data: Any) -> bool:
|
||||
if len(to) != 20:
|
||||
abort()
|
||||
|
||||
token_info = _readToken(token_id)
|
||||
from_addr = token_info['owner']
|
||||
|
||||
if not check_witness(from_addr):
|
||||
abort()
|
||||
|
||||
if from_addr == to:
|
||||
return True
|
||||
|
||||
_removeTokenOf(from_addr, token_id)
|
||||
_addTokenOf(to, token_id)
|
||||
|
||||
token_info['owner'] = to
|
||||
put(TOKEN_PREFIX + token_id.encode(), _serialize_token(token_info))
|
||||
return True
|
||||
|
||||
@public
|
||||
def mint(token_id: str, name: str, blurred_ref: str, full_ref: str, price: int) -> bool:
|
||||
admin = get(ADMIN_KEY)
|
||||
if not admin or len(admin) != 20:
|
||||
abort()
|
||||
|
||||
if not check_witness(admin.to_uint160()):
|
||||
abort()
|
||||
|
||||
existing = get(TOKEN_PREFIX + token_id.encode())
|
||||
if existing:
|
||||
abort() # уже есть
|
||||
|
||||
owner = admin.to_uint160()
|
||||
|
||||
info = {
|
||||
"owner": owner,
|
||||
"name": name,
|
||||
"blurred_ref": blurred_ref,
|
||||
"full_ref": full_ref,
|
||||
"price": price
|
||||
}
|
||||
put(TOKEN_PREFIX + token_id.encode(), _serialize_token(info))
|
||||
|
||||
current_supply = totalSupply()
|
||||
put(SUPPLY_KEY, current_supply + 1)
|
||||
_addTokenOf(owner, token_id)
|
||||
|
||||
return True
|
||||
|
||||
@public
|
||||
def deploy(admin: UInt160):
|
||||
if totalSupply() != 0:
|
||||
return
|
||||
put(ADMIN_KEY, admin)
|
||||
|
||||
def _readToken(token_id: str) -> Dict[str, Any]:
|
||||
token_data = get(TOKEN_PREFIX + token_id.encode())
|
||||
if not token_data:
|
||||
abort()
|
||||
return _deserialize_token(token_data)
|
||||
|
||||
def _getTokensOf(owner: UInt160) -> List[str]:
|
||||
prefix = OWNER_PREFIX + owner
|
||||
result: List[str] = []
|
||||
iterator = find(prefix, options=FindOptions.KeysOnly)
|
||||
while iterator.next():
|
||||
key = iterator.value
|
||||
token_id = key[len(prefix):].decode()
|
||||
result.append(token_id)
|
||||
return result
|
||||
|
||||
def _addTokenOf(owner: UInt160, token_id: str):
|
||||
owner_key = OWNER_PREFIX + owner + token_id.encode()
|
||||
put(owner_key, b'1')
|
||||
|
||||
def _removeTokenOf(owner: UInt160, token_id: str):
|
||||
owner_key = OWNER_PREFIX + owner + token_id.encode()
|
||||
delete(owner_key)
|
||||
|
||||
def _serialize_token(token_info: Dict[str, Any]) -> bytes:
|
||||
from boa3.builtin import serialize
|
||||
return serialize(token_info)
|
||||
|
||||
def _deserialize_token(data: bytes) -> Dict[str, Any]:
|
||||
from boa3.builtin import deserialize
|
||||
return deserialize(data)
|
77
contracts/nep17/MyNeoToken.py
Normal file
77
contracts/nep17/MyNeoToken.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from typing import Any
|
||||
from boa3.builtin import public
|
||||
from boa3.builtin.contract import Nep17TransferEvent, abort
|
||||
from boa3.builtin.interop.contract import call_contract
|
||||
from boa3.builtin.interop.runtime import calling_script_hash, check_witness, executing_script_hash
|
||||
from boa3.builtin.interop.storage import get, put
|
||||
from boa3.builtin.type import UInt160
|
||||
|
||||
# ------------------------------------------------------
|
||||
# Конфигурация токена
|
||||
# ------------------------------------------------------
|
||||
TOKEN_SYMBOL = 'MYNEO'
|
||||
TOKEN_DECIMALS = 0
|
||||
|
||||
TOTAL_SUPPLY_KEY = b'totalSupply'
|
||||
BALANCE_PREFIX = b'balance_'
|
||||
ADMIN_KEY = b'admin' # владелец смарт-контракта
|
||||
|
||||
on_transfer = Nep17TransferEvent
|
||||
|
||||
@public
|
||||
def symbol() -> str:
|
||||
return TOKEN_SYMBOL
|
||||
|
||||
@public
|
||||
def decimals() -> int:
|
||||
return TOKEN_DECIMALS
|
||||
|
||||
@public
|
||||
def totalSupply() -> int:
|
||||
return get(TOTAL_SUPPLY_KEY).to_int()
|
||||
|
||||
@public
|
||||
def balanceOf(account: UInt160) -> int:
|
||||
return get(BALANCE_PREFIX + account).to_int()
|
||||
|
||||
@public
|
||||
def transfer(from_addr: UInt160, to_addr: UInt160, amount: int, data: Any) -> bool:
|
||||
if amount < 0:
|
||||
abort()
|
||||
|
||||
if not check_witness(from_addr):
|
||||
abort()
|
||||
|
||||
if len(to_addr) != 20:
|
||||
abort()
|
||||
|
||||
from_balance = balanceOf(from_addr)
|
||||
if from_balance < amount:
|
||||
abort()
|
||||
|
||||
if from_addr != to_addr and amount != 0:
|
||||
put(BALANCE_PREFIX + from_addr, from_balance - amount)
|
||||
to_balance = balanceOf(to_addr)
|
||||
put(BALANCE_PREFIX + to_addr, to_balance + amount)
|
||||
|
||||
on_transfer(from_addr, to_addr, amount)
|
||||
|
||||
if data is not None:
|
||||
# Вызываем onNEP17Payment, если у целевого контракта есть такой метод
|
||||
call_contract(to_addr, 'onNEP17Payment', [from_addr, amount, data])
|
||||
|
||||
return True
|
||||
|
||||
@public
|
||||
def deploy(admin: UInt160):
|
||||
"""
|
||||
При первом деплое: устанавливаем админа и минтим 100_000_000 токенов.
|
||||
"""
|
||||
if totalSupply() != 0:
|
||||
return # уже инициализировано
|
||||
|
||||
put(ADMIN_KEY, admin)
|
||||
minted = 100_000_000
|
||||
put(TOTAL_SUPPLY_KEY, minted)
|
||||
put(BALANCE_PREFIX + admin, minted)
|
||||
on_transfer(None, admin, minted)
|
75
docker-compose.yml
Normal file
75
docker-compose.yml
Normal file
|
@ -0,0 +1,75 @@
|
|||
version: "3.8"
|
||||
services:
|
||||
|
||||
# NEO приватная нода для локальной разработки
|
||||
neo-node:
|
||||
image: nspccdev/neo-go
|
||||
container_name: neo-node
|
||||
command: [
|
||||
"node",
|
||||
"--network=priv",
|
||||
"--rpc",
|
||||
"--rpc-port=20332",
|
||||
"--wallet", "/neo-go/configs/wallet.json",
|
||||
"--unlock", "address_in_wallet",
|
||||
"--wallet-password", "pass"
|
||||
]
|
||||
ports:
|
||||
- "20332:20332"
|
||||
volumes:
|
||||
- ./neo-config:/neo-go/configs
|
||||
|
||||
# FrostFS (локальный dev-кластер или удаленный endpoint)
|
||||
# Для примера укажем некий самопальный образ,
|
||||
# в реальности нужна полноценная конфигурация.
|
||||
frostfs:
|
||||
image: yourorg/frostfs-dev
|
||||
container_name: frostfs
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
- SOME_ENV=...
|
||||
# ...
|
||||
|
||||
# Сервис для загрузки в FrostFS
|
||||
frostfs-uploader:
|
||||
build:
|
||||
context: ./services/frostfs-uploader
|
||||
container_name: frostfs-uploader
|
||||
environment:
|
||||
- FROSTFS_ENDPOINT=grpcs://frostfs:8080
|
||||
- FROSTFS_PRIVKEY=... # dev key
|
||||
- FROSTFS_CONTAINER_ID=... # dev container ID
|
||||
ports:
|
||||
- "8081:8081"
|
||||
depends_on:
|
||||
- frostfs
|
||||
|
||||
# API-сервис
|
||||
api:
|
||||
build:
|
||||
context: ./services/api
|
||||
container_name: onlyfans-api
|
||||
environment:
|
||||
- NEO_RPC_ENDPOINT=http://neo-node:20332
|
||||
- NEO_WALLET_PATH=/app/wallets/dev.wallet.json
|
||||
- NEO_WALLET_PASS=somepass
|
||||
- NFT_CONTRACT_HASH=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
||||
- MARKET_CONTRACT_HASH=cccccccccccccccccccccccccccccccccccccccc
|
||||
volumes:
|
||||
- ./services/api/wallets:/app/wallets
|
||||
ports:
|
||||
- "8080:8080"
|
||||
depends_on:
|
||||
- neo-node
|
||||
|
||||
# Фронтенд
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
container_name: onlyfans-frontend
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
- api
|
||||
- frostfs-uploader
|
95
services/api/internal/handlers/nft_handler.go
Normal file
95
services/api/internal/handlers/nft_handler.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"myproject/services/api/internal/neo"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
)
|
||||
|
||||
// Пример: /nft/mint?token_id=myPost1&blurred=OID1&full=OID2&price=100
|
||||
func HandleMintNFT(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient) {
|
||||
// Получаем параметры
|
||||
tokenId := r.URL.Query().Get("token_id")
|
||||
blurred := r.URL.Query().Get("blurred")
|
||||
full := r.URL.Query().Get("full")
|
||||
priceStr := r.URL.Query().Get("price")
|
||||
if tokenId == "" || blurred == "" || full == "" {
|
||||
http.Error(w, "missing params", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Конвертируем price
|
||||
// (в примере NEP-17 без decimals, так что не умножаем на 10^дробность)
|
||||
price := big.NewInt(0)
|
||||
price.SetString(priceStr, 10)
|
||||
|
||||
// Хэш NFT-контракта из ENV
|
||||
nftHashStr := os.Getenv("NFT_CONTRACT_HASH")
|
||||
nftHash, err := util.Uint160DecodeStringLE(nftHashStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid NFT hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Вызываем метод "mint"
|
||||
txHash, err := neoCli.Actor.Call(nftHash, "mint", []any{
|
||||
tokenId, // str
|
||||
"My NFT", // name (можно отдельно передавать)
|
||||
blurred, // blurred_ref
|
||||
full, // full_ref
|
||||
price.Int64(),
|
||||
}, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "mint call error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
"status": "ok",
|
||||
"tx_hash": txHash.StringBE(),
|
||||
})
|
||||
}
|
||||
|
||||
func HandleListMarket(w http.ResponseWriter, r *http.Request, neoCli *neo.NeoClient) {
|
||||
marketHashStr := os.Getenv("MARKET_CONTRACT_HASH")
|
||||
marketHash, err := util.Uint160DecodeStringLE(marketHashStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid MARKET hash", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Invoke без отправки транзакции (чтение из стейта)
|
||||
res, err := neoCli.Actor.Reader().InvokeCall(marketHash, "List", []any{}, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "List call error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// res.Value — stackitem
|
||||
// Для простоты декодируем
|
||||
tokenIDs, err := res.Stack[0].TryArray()
|
||||
if err != nil {
|
||||
http.Error(w, "decoding array error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
list := []string{}
|
||||
for _, t := range tokenIDs {
|
||||
bs, err := t.TryBytes()
|
||||
if err == nil {
|
||||
list = append(list, string(bs))
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]any{
|
||||
"status": "ok",
|
||||
"onSale": list,
|
||||
})
|
||||
}
|
47
services/api/internal/neo/neo_client.go
Normal file
47
services/api/internal/neo/neo_client.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package neo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
|
||||
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
|
||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||
)
|
||||
|
||||
type NeoClient struct {
|
||||
Actor *actor.Actor
|
||||
RPC *rpcclient.Client
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
func NewNeoClient(rpcEndpoint, walletPath, walletPass string) (*NeoClient, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
rpcCli, err := rpcclient.New(ctx, rpcEndpoint, rpcclient.Options{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("rpcclient new: %w", err)
|
||||
}
|
||||
|
||||
w, err := wallet.NewWalletFromFile(walletPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wallet load: %w", err)
|
||||
}
|
||||
|
||||
acc := w.GetAccount(w.GetChangeAddress())
|
||||
err = acc.Decrypt(walletPass, w.Scrypt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wallet decrypt: %w", err)
|
||||
}
|
||||
|
||||
act, err := actor.NewSimple(rpcCli, acc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("actor new: %w", err)
|
||||
}
|
||||
|
||||
return &NeoClient{
|
||||
Actor: act,
|
||||
RPC: rpcCli,
|
||||
Context: ctx,
|
||||
}, nil
|
||||
}
|
38
services/api/main.go
Normal file
38
services/api/main.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"myproject/services/api/internal/handlers"
|
||||
"myproject/services/api/internal/neo"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
rpcEndpoint := os.Getenv("NEO_RPC_ENDPOINT") // "http://localhost:20332"
|
||||
walletPath := os.Getenv("NEO_WALLET_PATH")
|
||||
walletPass := os.Getenv("NEO_WALLET_PASS")
|
||||
|
||||
// Инициализируем клиент NEO
|
||||
neoCli, err := neo.NewNeoClient(rpcEndpoint, walletPath, walletPass)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to init neo client: %v", err)
|
||||
}
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/nft/mint", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.HandleMintNFT(w, r, neoCli)
|
||||
}).Methods("POST")
|
||||
|
||||
r.HandleFunc("/nft/market-list", func(w http.ResponseWriter, r *http.Request) {
|
||||
handlers.HandleListMarket(w, r, neoCli)
|
||||
}).Methods("GET")
|
||||
|
||||
// и т.д.
|
||||
|
||||
log.Println("Starting API on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", r))
|
||||
}
|
99
services/frostfs-uploader/main.go
Normal file
99
services/frostfs-uploader/main.go
Normal file
|
@ -0,0 +1,99 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/client"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/client/object"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
// ...
|
||||
)
|
||||
|
||||
var (
|
||||
frostfsEndpoint = os.Getenv("FROSTFS_ENDPOINT") // типа "grpcs://localhost:8080"
|
||||
frostfsPrivKey = os.Getenv("FROSTFS_PRIVKEY")
|
||||
frostfsContainer = os.Getenv("FROSTFS_CONTAINER_ID")
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Инициализация
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/upload", handleUpload).Methods("POST")
|
||||
|
||||
// HTTP-сервер
|
||||
addr := ":8081"
|
||||
log.Println("Starting FrostFS uploader on", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, r))
|
||||
}
|
||||
|
||||
func handleUpload(w http.ResponseWriter, r *http.Request) {
|
||||
// Пример: берем файл из multipart/form-data
|
||||
file, header, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, "file error: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Читаем в []byte (для простоты, но лучше стримить)
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, "read error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Подключаемся к FrostFS
|
||||
cli, err := client.New(client.WithDefaultPrivateKeyStr(frostfsPrivKey))
|
||||
if err != nil {
|
||||
http.Error(w, "client init error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
err = cli.Dial(frostfsEndpoint)
|
||||
if err != nil {
|
||||
http.Error(w, "dial error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
cntr, err := container.IDFromString(frostfsContainer)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid container: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
oid, err := uploadFileToFrostFS(r.Context(), cli, cntr, data)
|
||||
if err != nil {
|
||||
http.Error(w, "frostfs upload error: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprintf(w, `{"status":"ok","object_id":"%s","filename":"%s"}`, oid, header.Filename)
|
||||
}
|
||||
|
||||
func uploadFileToFrostFS(ctx context.Context, cli *client.Client, cntr container.ID, data []byte) (string, error) {
|
||||
obj := object.New()
|
||||
obj.SetPayload(data)
|
||||
|
||||
writer, err := cli.ObjectPutInit(ctx, cntr, obj)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
err = writer.Write(data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
oid, err := writer.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return oid.String(), nil
|
||||
}
|
Loading…
Add table
Reference in a new issue