mirror of
https://github.com/wgh136/nysoure.git
synced 2025-09-27 12:17:24 +00:00
Compare commits
7 Commits
f3b3f2bd5a
...
634d5a348a
Author | SHA1 | Date | |
---|---|---|---|
634d5a348a | |||
488a91e651 | |||
3dd042a752 | |||
43af0412ef | |||
b17fa45d79 | |||
1925cf404e | |||
77ad261670 |
@@ -115,7 +115,7 @@ article {
|
|||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, system-ui;
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, system-ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
pre code {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: block;
|
display: block;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
@@ -162,6 +162,7 @@ export interface ServerConfig {
|
|||||||
allow_normal_user_upload: boolean;
|
allow_normal_user_upload: boolean;
|
||||||
max_normal_user_upload_size_in_mb: number;
|
max_normal_user_upload_size_in_mb: number;
|
||||||
upload_prompt: string;
|
upload_prompt: string;
|
||||||
|
pinned_resources: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum RSort {
|
export enum RSort {
|
||||||
|
@@ -415,6 +415,10 @@ class Network {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getPinnedResources(): Promise<Response<Resource[]>> {
|
||||||
|
return this._callApi(() => axios.get(`${this.apiBaseUrl}/resource/pinned`));
|
||||||
|
}
|
||||||
|
|
||||||
async createS3Storage(
|
async createS3Storage(
|
||||||
name: string,
|
name: string,
|
||||||
endPoint: string,
|
endPoint: string,
|
||||||
|
@@ -95,7 +95,9 @@ function ActivityCard({ activity }: { activity: Activity }) {
|
|||||||
) {
|
) {
|
||||||
content = (
|
content = (
|
||||||
<div className={"mx-1"}>
|
<div className={"mx-1"}>
|
||||||
<div className={"font-bold my-4 break-all"}>{activity.resource?.title}</div>
|
<div className={"font-bold my-4 break-all"}>
|
||||||
|
{activity.resource?.title}
|
||||||
|
</div>
|
||||||
{activity.resource?.image && (
|
{activity.resource?.image && (
|
||||||
<div>
|
<div>
|
||||||
<img
|
<img
|
||||||
@@ -116,7 +118,9 @@ function ActivityCard({ activity }: { activity: Activity }) {
|
|||||||
} else if (activity.type === ActivityType.NewFile) {
|
} else if (activity.type === ActivityType.NewFile) {
|
||||||
content = (
|
content = (
|
||||||
<div>
|
<div>
|
||||||
<h4 className={"font-bold py-2 break-all"}>{activity.file!.filename}</h4>
|
<h4 className={"font-bold py-2 break-all"}>
|
||||||
|
{activity.file!.filename}
|
||||||
|
</h4>
|
||||||
<div className={"text-sm my-1 comment_tile"}>
|
<div className={"text-sm my-1 comment_tile"}>
|
||||||
<Markdown>
|
<Markdown>
|
||||||
{activity.file!.description.replaceAll("\n", " \n")}
|
{activity.file!.description.replaceAll("\n", " \n")}
|
||||||
@@ -170,8 +174,12 @@ function ActivityCard({ activity }: { activity: Activity }) {
|
|||||||
src={network.getUserAvatar(activity.user!)}
|
src={network.getUserAvatar(activity.user!)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className={"mx-2 font-bold text-sm"}>{activity.user?.username}</span>
|
<span className={"mx-2 font-bold text-sm"}>
|
||||||
<span className={"ml-2 badge-sm sm:badge-md badge badge-primary badge-soft"}>
|
{activity.user?.username}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={"ml-2 badge-sm sm:badge-md badge badge-primary badge-soft"}
|
||||||
|
>
|
||||||
{messages[activity.type]}
|
{messages[activity.type]}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -164,7 +164,8 @@ export default function CollectionPage() {
|
|||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
{!collection.isPublic && (
|
{!collection.isPublic && (
|
||||||
<Badge className="badge-soft badge-error text-xs mr-2 shadow-xs">
|
<Badge className="badge-soft badge-error text-xs mr-2 shadow-xs">
|
||||||
<MdOutlineLock size={16} className="inline-block" /> {t("Private")}
|
<MdOutlineLock size={16} className="inline-block" />{" "}
|
||||||
|
{t("Private")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@@ -2,10 +2,12 @@ import { useEffect, useState } from "react";
|
|||||||
import ResourcesView from "../components/resources_view.tsx";
|
import ResourcesView from "../components/resources_view.tsx";
|
||||||
import { network } from "../network/network.ts";
|
import { network } from "../network/network.ts";
|
||||||
import { app } from "../app.ts";
|
import { app } from "../app.ts";
|
||||||
import { RSort } from "../network/models.ts";
|
import { Resource, RSort } from "../network/models.ts";
|
||||||
import { useTranslation } from "../utils/i18n";
|
import { useTranslation } from "../utils/i18n";
|
||||||
import { useAppContext } from "../components/AppContext.tsx";
|
import { useAppContext } from "../components/AppContext.tsx";
|
||||||
import Select from "../components/select.tsx";
|
import Select from "../components/select.tsx";
|
||||||
|
import { useNavigate } from "react-router";
|
||||||
|
import { useNavigator } from "../components/navigator.tsx";
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -31,6 +33,7 @@ export default function HomePage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<PinnedResources />
|
||||||
<div className={"flex pt-4 px-4 items-center"}>
|
<div className={"flex pt-4 px-4 items-center"}>
|
||||||
<Select
|
<Select
|
||||||
values={[
|
values={[
|
||||||
@@ -58,3 +61,80 @@ export default function HomePage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cachedPinnedResources: Resource[] | null = null;
|
||||||
|
|
||||||
|
function PinnedResources() {
|
||||||
|
const [pinnedResources, setPinnedResources] = useState<Resource[]>([]);
|
||||||
|
const navigator = useNavigator();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cachedPinnedResources != null) {
|
||||||
|
setPinnedResources(cachedPinnedResources);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prefetchData = app.getPreFetchData();
|
||||||
|
if (prefetchData && prefetchData.background) {
|
||||||
|
navigator.setBackground(network.getResampledImageUrl(prefetchData.background));
|
||||||
|
}
|
||||||
|
if (prefetchData && prefetchData.pinned) {
|
||||||
|
cachedPinnedResources = prefetchData.pinned;
|
||||||
|
setPinnedResources(cachedPinnedResources!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fetchPinnedResources = async () => {
|
||||||
|
const res = await network.getPinnedResources();
|
||||||
|
if (res.success) {
|
||||||
|
cachedPinnedResources = res.data ?? [];
|
||||||
|
setPinnedResources(res.data ?? []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchPinnedResources();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (pinnedResources.length == 0) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||||
|
{pinnedResources.map((resource) => (
|
||||||
|
<PinnedResourceItem key={resource.id} resource={resource} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PinnedResourceItem({ resource }: { resource: Resource }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`/resources/${resource.id}`}
|
||||||
|
className={"p-2 cursor-pointer block"}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
navigate(`/resources/${resource.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
"card shadow hover:shadow-md transition-shadow bg-base-100-tr82"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{resource.image != null && (
|
||||||
|
<figure>
|
||||||
|
<img
|
||||||
|
src={network.getResampledImageUrl(resource.image.id)}
|
||||||
|
alt="cover"
|
||||||
|
className="w-full aspect-[5/2] object-cover"
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
)}
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="card-title break-all">{resource.title}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@@ -14,12 +14,15 @@ export default function ManageServerConfigPage() {
|
|||||||
|
|
||||||
const [config, setConfig] = useState<ServerConfig | null>(null);
|
const [config, setConfig] = useState<ServerConfig | null>(null);
|
||||||
|
|
||||||
|
const [pinnedResources, setPinnedResources] = useState("");
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
network.getServerConfig().then((res) => {
|
network.getServerConfig().then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setConfig(res.data!);
|
setConfig(res.data!);
|
||||||
|
setPinnedResources(res.data!.pinned_resources.join(","));
|
||||||
} else {
|
} else {
|
||||||
showToast({
|
showToast({
|
||||||
message: res.message,
|
message: res.message,
|
||||||
@@ -56,8 +59,25 @@ export default function ManageServerConfigPage() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
function isPositiveInteger(str: string) {
|
||||||
|
return /^[1-9]\d*$/.test(str);
|
||||||
|
}
|
||||||
|
for (const e of pinnedResources.split(",")) {
|
||||||
|
if (!isPositiveInteger(e)) {
|
||||||
|
showToast({
|
||||||
|
message: "Pinned resources must be a comma separated list of numbers",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let pinned = pinnedResources.split(",").map((id) => parseInt(id));
|
||||||
|
setConfig({ ...config, pinned_resources: pinned });
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const res = await network.setServerConfig(config);
|
const res = await network.setServerConfig({
|
||||||
|
...config,
|
||||||
|
pinned_resources: pinned,
|
||||||
|
});
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
showToast({
|
showToast({
|
||||||
message: t("Update server config successfully"),
|
message: t("Update server config successfully"),
|
||||||
@@ -197,6 +217,14 @@ export default function ManageServerConfigPage() {
|
|||||||
setConfig({ ...config, upload_prompt: e.target.value });
|
setConfig({ ...config, upload_prompt: e.target.value });
|
||||||
}}
|
}}
|
||||||
></Input>
|
></Input>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={pinnedResources}
|
||||||
|
label="Pinned resources"
|
||||||
|
onChange={(e) => {
|
||||||
|
setPinnedResources(e.target.value);
|
||||||
|
}}
|
||||||
|
></Input>
|
||||||
<InfoAlert
|
<InfoAlert
|
||||||
className="my-2"
|
className="my-2"
|
||||||
message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download."
|
message="If the cloudflare turnstile keys are not empty, the turnstile will be used for register and download."
|
||||||
|
@@ -217,7 +217,8 @@ export default function ResourcePage() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<Tags tags={resource.tags} />
|
<Tags tags={resource.tags} />
|
||||||
<p className={"px-3 mt-2"}>
|
|
||||||
|
<div className={"px-3 mt-2 flex flex-wrap"}>
|
||||||
{resource.links &&
|
{resource.links &&
|
||||||
resource.links.map((l) => {
|
resource.links.map((l) => {
|
||||||
return (
|
return (
|
||||||
@@ -238,7 +239,7 @@ export default function ResourcePage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<CollectionDialog rid={resource.id} />
|
<CollectionDialog rid={resource.id} />
|
||||||
</p>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="tabs tabs-box my-4 mx-2 p-4 shadow"
|
className="tabs tabs-box my-4 mx-2 p-4 shadow"
|
||||||
|
@@ -48,7 +48,13 @@ func setServerConfig(c fiber.Ctx) error {
|
|||||||
return model.NewRequestError("Invalid request parameters")
|
return model.NewRequestError("Invalid request parameters")
|
||||||
}
|
}
|
||||||
|
|
||||||
config.SetConfig(sc)
|
if err := config.SetConfig(sc); err != nil {
|
||||||
|
return model.NewInternalServerError("Failed to save configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.Validate(); err != nil {
|
||||||
|
return model.NewRequestError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return c.JSON(model.Response[any]{
|
return c.JSON(model.Response[any]{
|
||||||
Success: true,
|
Success: true,
|
||||||
|
@@ -267,6 +267,21 @@ func handleGetRandomResource(c fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleGetPinnedResources(c fiber.Ctx) error {
|
||||||
|
views, err := service.GetPinnedResources()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if views == nil {
|
||||||
|
views = []model.ResourceView{}
|
||||||
|
}
|
||||||
|
return c.Status(fiber.StatusOK).JSON(model.Response[[]model.ResourceView]{
|
||||||
|
Success: true,
|
||||||
|
Data: views,
|
||||||
|
Message: "Pinned resources retrieved successfully",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func AddResourceRoutes(api fiber.Router) {
|
func AddResourceRoutes(api fiber.Router) {
|
||||||
resource := api.Group("/resource")
|
resource := api.Group("/resource")
|
||||||
{
|
{
|
||||||
@@ -274,6 +289,7 @@ func AddResourceRoutes(api fiber.Router) {
|
|||||||
resource.Get("/search", handleSearchResources)
|
resource.Get("/search", handleSearchResources)
|
||||||
resource.Get("/", handleListResources)
|
resource.Get("/", handleListResources)
|
||||||
resource.Get("/random", handleGetRandomResource)
|
resource.Get("/random", handleGetRandomResource)
|
||||||
|
resource.Get("/pinned", handleGetPinnedResources)
|
||||||
resource.Get("/:id", handleGetResource)
|
resource.Get("/:id", handleGetResource)
|
||||||
resource.Delete("/:id", handleDeleteResource)
|
resource.Delete("/:id", handleDeleteResource)
|
||||||
resource.Get("/tag/:tag", handleListResourcesWithTag)
|
resource.Get("/tag/:tag", handleListResourcesWithTag)
|
||||||
|
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"nysoure/server/utils"
|
"nysoure/server/utils"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -34,6 +35,30 @@ type ServerConfig struct {
|
|||||||
MaxNormalUserUploadSizeInMB int `json:"max_normal_user_upload_size_in_mb"`
|
MaxNormalUserUploadSizeInMB int `json:"max_normal_user_upload_size_in_mb"`
|
||||||
// Prompt for upload page
|
// Prompt for upload page
|
||||||
UploadPrompt string `json:"upload_prompt"`
|
UploadPrompt string `json:"upload_prompt"`
|
||||||
|
// PinnedResources is a list of resource IDs that are pinned to the top of the page.
|
||||||
|
PinnedResources []uint `json:"pinned_resources"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ServerConfig) Validate() error {
|
||||||
|
if c.MaxUploadingSizeInMB <= 0 {
|
||||||
|
return errors.New("MaxUploadingSizeInMB must be positive")
|
||||||
|
}
|
||||||
|
if c.MaxFileSizeInMB <= 0 {
|
||||||
|
return errors.New("MaxFileSizeInMB must be positive")
|
||||||
|
}
|
||||||
|
if c.MaxDownloadsPerDayForSingleIP <= 0 {
|
||||||
|
return errors.New("MaxDownloadsPerDayForSingleIP must be positive")
|
||||||
|
}
|
||||||
|
if c.ServerName == "" {
|
||||||
|
return errors.New("ServerName must not be empty")
|
||||||
|
}
|
||||||
|
if c.ServerDescription == "" {
|
||||||
|
return errors.New("ServerDescription must not be empty")
|
||||||
|
}
|
||||||
|
if len(c.PinnedResources) > 8 {
|
||||||
|
return errors.New("PinnedResources must not exceed 8 items")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -51,6 +76,7 @@ func init() {
|
|||||||
AllowNormalUserUpload: true,
|
AllowNormalUserUpload: true,
|
||||||
MaxNormalUserUploadSizeInMB: 16,
|
MaxNormalUserUploadSizeInMB: 16,
|
||||||
UploadPrompt: "You can upload your files here.",
|
UploadPrompt: "You can upload your files here.",
|
||||||
|
PinnedResources: []uint{},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
data, err := os.ReadFile(p)
|
data, err := os.ReadFile(p)
|
||||||
@@ -68,16 +94,17 @@ func GetConfig() ServerConfig {
|
|||||||
return *config
|
return *config
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetConfig(newConfig ServerConfig) {
|
func SetConfig(newConfig ServerConfig) error {
|
||||||
config = &newConfig
|
config = &newConfig
|
||||||
data, err := json.MarshalIndent(config, "", " ")
|
data, err := json.MarshalIndent(config, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
p := filepath.Join(utils.GetStoragePath(), "config.json")
|
p := filepath.Join(utils.GetStoragePath(), "config.json")
|
||||||
if err := os.WriteFile(p, data, 0644); err != nil {
|
if err := os.WriteFile(p, data, 0644); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MaxUploadingSize() int64 {
|
func MaxUploadingSize() int64 {
|
||||||
@@ -127,3 +154,7 @@ func MaxNormalUserUploadSize() int64 {
|
|||||||
func UploadPrompt() string {
|
func UploadPrompt() string {
|
||||||
return config.UploadPrompt
|
return config.UploadPrompt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PinnedResources() []uint {
|
||||||
|
return config.PinnedResources
|
||||||
|
}
|
||||||
|
@@ -155,6 +155,16 @@ func serveIndexHtml(c fiber.Ctx) error {
|
|||||||
preFetchData = url.PathEscape(string(preFetchDataJson))
|
preFetchData = url.PathEscape(string(preFetchDataJson))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if path == "/" || path == "" {
|
||||||
|
pinned, err := service.GetPinnedResources()
|
||||||
|
random, err1 := service.RandomCover()
|
||||||
|
if err == nil && err1 == nil {
|
||||||
|
preFetchDataJson, _ := json.Marshal(map[string]interface{}{
|
||||||
|
"pinned": pinned,
|
||||||
|
"background": random,
|
||||||
|
})
|
||||||
|
preFetchData = url.PathEscape(string(preFetchDataJson))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
content = strings.ReplaceAll(content, "{{SiteName}}", siteName)
|
content = strings.ReplaceAll(content, "{{SiteName}}", siteName)
|
||||||
|
@@ -445,32 +445,51 @@ func DownloadFile(fid, cfToken string) (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testFileUrl(url string) (int64, error) {
|
func testFileUrl(url string) (int64, error) {
|
||||||
client := http.Client{
|
client := http.Client{Timeout: 10 * time.Second}
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
}
|
// Try HEAD request first, fallback to GET
|
||||||
req, err := http.NewRequest("HEAD", url, nil)
|
for _, method := range []string{"HEAD", "GET"} {
|
||||||
|
req, err := http.NewRequest(method, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, model.NewRequestError("failed to create HTTP request")
|
return 0, model.NewRequestError("failed to create HTTP request")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if method == "GET" {
|
||||||
return 0, model.NewRequestError("failed to send HTTP request")
|
return 0, model.NewRequestError("failed to send HTTP request")
|
||||||
}
|
}
|
||||||
|
continue // Try GET if HEAD fails
|
||||||
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
if method == "GET" {
|
||||||
return 0, model.NewRequestError("URL is not accessible, status code: " + resp.Status)
|
return 0, model.NewRequestError("URL is not accessible, status code: " + resp.Status)
|
||||||
}
|
}
|
||||||
if resp.Header.Get("Content-Length") == "" {
|
continue // Try GET if HEAD fails
|
||||||
|
}
|
||||||
|
|
||||||
|
contentLengthStr := resp.Header.Get("Content-Length")
|
||||||
|
if contentLengthStr == "" {
|
||||||
|
if method == "GET" {
|
||||||
return 0, model.NewRequestError("URL does not provide content length")
|
return 0, model.NewRequestError("URL does not provide content length")
|
||||||
}
|
}
|
||||||
contentLength, err := strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 64)
|
continue // Try GET if HEAD doesn't provide Content-Length
|
||||||
if err != nil {
|
|
||||||
return 0, model.NewRequestError("failed to parse Content-Length header")
|
|
||||||
}
|
}
|
||||||
if contentLength <= 0 {
|
|
||||||
|
contentLength, err := strconv.ParseInt(contentLengthStr, 10, 64)
|
||||||
|
if err != nil || contentLength <= 0 {
|
||||||
|
if method == "GET" {
|
||||||
return 0, model.NewRequestError("Content-Length is not valid")
|
return 0, model.NewRequestError("Content-Length is not valid")
|
||||||
}
|
}
|
||||||
|
continue // Try GET if HEAD has invalid Content-Length
|
||||||
|
}
|
||||||
|
|
||||||
return contentLength, nil
|
return contentLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, model.NewRequestError("failed to get valid content length")
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadFile return nil if the download is successful or the context is cancelled
|
// downloadFile return nil if the download is successful or the context is cancelled
|
||||||
|
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"nysoure/server/config"
|
||||||
"nysoure/server/dao"
|
"nysoure/server/dao"
|
||||||
"nysoure/server/model"
|
"nysoure/server/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -286,3 +287,32 @@ func RandomResource(host string) (*model.ResourceDetailView, error) {
|
|||||||
}
|
}
|
||||||
return &v, nil
|
return &v, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var lastSuccessCover uint
|
||||||
|
|
||||||
|
func RandomCover() (uint, error) {
|
||||||
|
for retries := 0; retries < 5; retries++ {
|
||||||
|
v, err := dao.RandomResource()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(v.Images) > 0 {
|
||||||
|
lastSuccessCover = v.Images[0].ID
|
||||||
|
return v.Images[0].ID, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastSuccessCover, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPinnedResources() ([]model.ResourceView, error) {
|
||||||
|
ids := config.PinnedResources()
|
||||||
|
var views []model.ResourceView
|
||||||
|
for _, id := range ids {
|
||||||
|
r, err := dao.GetResourceByID(id)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
views = append(views, r.ToView())
|
||||||
|
}
|
||||||
|
return views, nil
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user