forked from Kieran/void.cat
Allow editing metadata
This commit is contained in:
parent
20d32ad6d3
commit
285899b742
@ -194,8 +194,7 @@ namespace VoidCat.Controllers
|
||||
var gid = id.FromBase58Guid();
|
||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||
if (meta == default) return NotFound();
|
||||
|
||||
if (req.EditSecret != meta.EditSecret) return Unauthorized();
|
||||
if (!meta.CanEdit(req.EditSecret, HttpContext)) return Unauthorized();
|
||||
|
||||
if (req.Strike != default)
|
||||
{
|
||||
@ -207,6 +206,28 @@ namespace VoidCat.Controllers
|
||||
await _paywall.Set(gid, new NoPaywallConfig());
|
||||
return Ok();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update metadata about file
|
||||
/// </summary>
|
||||
/// <param name="id">Id of file to edit</param>
|
||||
/// <param name="fileMeta">New metadata to update</param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// You can only change `Name`, `Description` and `MimeType`
|
||||
/// </remarks>
|
||||
[HttpPost]
|
||||
[Route("{id}/meta")]
|
||||
public async Task<IActionResult> UpdateFileMeta([FromRoute] string id, [FromBody] SecretVoidFileMeta fileMeta)
|
||||
{
|
||||
var gid = id.FromBase58Guid();
|
||||
var meta = await _metadata.Get<SecretVoidFileMeta>(gid);
|
||||
if (meta == default) return NotFound();
|
||||
if (!meta.CanEdit(fileMeta.EditSecret, HttpContext)) return Unauthorized();
|
||||
|
||||
await _metadata.Update(gid, fileMeta);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
|
@ -61,6 +61,12 @@ public static class Extensions
|
||||
return !string.IsNullOrEmpty(h.Value.ToString()) ? h.Value.ToString() : default;
|
||||
}
|
||||
|
||||
public static bool CanEdit(this SecretVoidFileMeta file, Guid? editSecret, HttpContext context)
|
||||
{
|
||||
return file.EditSecret == editSecret
|
||||
|| file.Uploader == context.GetUserId();
|
||||
}
|
||||
|
||||
public static string ToHex(this byte[] data)
|
||||
{
|
||||
return BitConverter.ToString(data).Replace("-", string.Empty).ToLower();
|
||||
|
@ -26,7 +26,7 @@ public record VoidFileMeta : IVoidFileMeta
|
||||
/// <summary>
|
||||
/// Filename
|
||||
/// </summary>
|
||||
public string? Name { get; init; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Size of the file in storage
|
||||
@ -41,12 +41,12 @@ public record VoidFileMeta : IVoidFileMeta
|
||||
/// <summary>
|
||||
/// Description about the file
|
||||
/// </summary>
|
||||
public string? Description { get; init; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The content type of the file
|
||||
/// </summary>
|
||||
public string? MimeType { get; init; }
|
||||
public string? MimeType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SHA-256 hash of the file
|
||||
|
@ -5,4 +5,5 @@ namespace VoidCat.Services.Abstractions;
|
||||
public interface IFileMetadataStore : IPublicPrivateStore<VoidFileMeta, SecretVoidFileMeta>
|
||||
{
|
||||
ValueTask<TMeta?> Get<TMeta>(Guid id) where TMeta : VoidFileMeta;
|
||||
ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : VoidFileMeta;
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using VoidCat.Model;
|
||||
using VoidCat.Model.Exceptions;
|
||||
using VoidCat.Services.Abstractions;
|
||||
|
||||
namespace VoidCat.Services.Files;
|
||||
@ -28,6 +27,18 @@ public class LocalDiskFileMetadataStore : IFileMetadataStore
|
||||
return GetMeta<TMeta>(id);
|
||||
}
|
||||
|
||||
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : VoidFileMeta
|
||||
{
|
||||
var oldMeta = await GetMeta<SecretVoidFileMeta>(id);
|
||||
if (oldMeta == default) return;
|
||||
|
||||
oldMeta.Description = meta.Description ?? oldMeta.Description;
|
||||
oldMeta.Name = meta.Name ?? oldMeta.Name;
|
||||
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
|
||||
|
||||
await Set(id, oldMeta);
|
||||
}
|
||||
|
||||
public ValueTask<VoidFileMeta?> Get(Guid id)
|
||||
{
|
||||
return GetMeta<VoidFileMeta>(id);
|
||||
|
@ -25,6 +25,18 @@ public class S3FileMetadataStore : IFileMetadataStore
|
||||
return GetMeta<TMeta>(id);
|
||||
}
|
||||
|
||||
public async ValueTask Update<TMeta>(Guid id, TMeta meta) where TMeta : VoidFileMeta
|
||||
{
|
||||
var oldMeta = await GetMeta<SecretVoidFileMeta>(id);
|
||||
if (oldMeta == default) return;
|
||||
|
||||
oldMeta.Description = meta.Description ?? oldMeta.Description;
|
||||
oldMeta.Name = meta.Name ?? oldMeta.Name;
|
||||
oldMeta.MimeType = meta.MimeType ?? oldMeta.MimeType;
|
||||
|
||||
await Set(id, oldMeta);
|
||||
}
|
||||
|
||||
public ValueTask<VoidFileMeta?> Get(Guid id)
|
||||
{
|
||||
return GetMeta<VoidFileMeta>(id);
|
||||
|
@ -41,7 +41,8 @@ export function useApi() {
|
||||
updateUser: (u) => getJson("POST", `/user/${u.id}`, u, auth),
|
||||
listUserFiles: (uid, pageReq) => getJson("POST", `/user/${uid}/files`, pageReq, auth),
|
||||
submitVerifyCode: (uid, code) => getJson("POST", `/user/${uid}/verify`, code, auth),
|
||||
sendNewCode: (uid) => getJson("GET", `/user/${uid}/verify`, undefined, auth)
|
||||
sendNewCode: (uid) => getJson("GET", `/user/${uid}/verify`, undefined, auth),
|
||||
updateMetadata: (id, meta) => getJson("POST", `/upload/${id}/meta`, meta, auth)
|
||||
}
|
||||
};
|
||||
}
|
@ -5,6 +5,7 @@ import {NoPaywallConfig} from "./NoPaywallConfig";
|
||||
import {useApi} from "./Api";
|
||||
import "./FileEdit.css";
|
||||
import {useSelector} from "react-redux";
|
||||
import {VoidButton} from "./VoidButton";
|
||||
|
||||
export function FileEdit(props) {
|
||||
const {Api} = useApi();
|
||||
@ -25,6 +26,15 @@ export function FileEdit(props) {
|
||||
return req.ok;
|
||||
}
|
||||
|
||||
async function saveMeta() {
|
||||
let meta = {
|
||||
name,
|
||||
description,
|
||||
editSecret: privateFile?.metadata?.editSecret
|
||||
};
|
||||
await Api.updateMetadata(file.id, meta);
|
||||
}
|
||||
|
||||
function renderPaywallConfig() {
|
||||
switch (paywall) {
|
||||
case 0: {
|
||||
@ -47,7 +57,7 @@ export function FileEdit(props) {
|
||||
<dt>Description:</dt>
|
||||
<dd><input type="text" value={description} onChange={(e) => setDescription(e.target.value)}/></dd>
|
||||
</dl>
|
||||
|
||||
<VoidButton onClick={(e) => saveMeta()} options={{showSuccess: true}}>Save</VoidButton>
|
||||
</div>
|
||||
<div className="flx-1">
|
||||
<h3>Paywall Config</h3>
|
||||
|
@ -8,14 +8,12 @@ import {logout, setProfile as setGlobalProfile} from "./LoginState";
|
||||
import {DigestAlgo} from "./FileUpload";
|
||||
import {buf2hex, hasFlag} from "./Util";
|
||||
import moment from "moment";
|
||||
import FeatherIcon from "feather-icons-react";
|
||||
import {FileList} from "./FileList";
|
||||
import {VoidButton} from "./VoidButton";
|
||||
|
||||
export function Profile() {
|
||||
const [profile, setProfile] = useState();
|
||||
const [noProfile, setNoProfile] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [emailCode, setEmailCode] = useState("");
|
||||
const [emailCodeError, setEmailCodeError] = useState("");
|
||||
const [newCodeSent, setNewCodeSent] = useState(false);
|
||||
@ -103,7 +101,6 @@ export function Profile() {
|
||||
if (r.ok) {
|
||||
// saved
|
||||
dispatch(setGlobalProfile(profile));
|
||||
setSaved(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,10 +157,7 @@ export function Profile() {
|
||||
</dl>
|
||||
<div className="flex flex-center">
|
||||
<div>
|
||||
<VoidButton onClick={saveUser}>Save</VoidButton>
|
||||
</div>
|
||||
<div>
|
||||
{saved ? <FeatherIcon icon="check-circle"/> : null}
|
||||
<VoidButton onClick={saveUser} options={{showSuccess: true}}>Save</VoidButton>
|
||||
</div>
|
||||
<div>
|
||||
<VoidButton onClick={() => dispatch(logout())}>Logout</VoidButton>
|
||||
@ -177,12 +171,6 @@ export function Profile() {
|
||||
loadProfile();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (saved === true) {
|
||||
setTimeout(() => setSaved(false), 1000);
|
||||
}
|
||||
}, [saved]);
|
||||
|
||||
if (profile) {
|
||||
let avatarUrl = profile.avatar ?? DefaultAvatar;
|
||||
if (!avatarUrl.startsWith("http")) {
|
||||
|
@ -1,4 +1,13 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import FeatherIcon from "feather-icons-react";
|
||||
|
||||
export function VoidButton(props) {
|
||||
const options = {
|
||||
showSuccess: false,
|
||||
...props.options
|
||||
};
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
async function handleClick(e) {
|
||||
if (e.target.classList.contains("disabled")) return;
|
||||
e.target.classList.add("disabled");
|
||||
@ -9,10 +18,24 @@ export function VoidButton(props) {
|
||||
if (typeof ret === "object" && typeof ret.then === "function") {
|
||||
await ret;
|
||||
}
|
||||
setSuccess(options.showSuccess);
|
||||
}
|
||||
|
||||
e.target.classList.remove("disabled");
|
||||
}
|
||||
|
||||
return <div className="btn" onClick={handleClick}>{props.children}</div>;
|
||||
useEffect(() => {
|
||||
if (success === true) {
|
||||
setTimeout(() => setSuccess(false), 1000);
|
||||
}
|
||||
}, [success]);
|
||||
|
||||
return (
|
||||
<div className="flex-inline flex-center">
|
||||
<div>
|
||||
<div className="btn" onClick={handleClick}>{props.children}</div>
|
||||
</div>
|
||||
{success ? <div><FeatherIcon icon="check-circle"/></div> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -40,6 +40,10 @@ a:hover {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-inline {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.flx-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user