diff --git a/src/client/pages/gallery/edit.vue b/src/client/pages/gallery/edit.vue
new file mode 100644
index 0000000000..cd6a0defdd
--- /dev/null
+++ b/src/client/pages/gallery/edit.vue
@@ -0,0 +1,168 @@
+<template>
+<FormBase>
+	<FormSuspense :p="init">
+		<FormInput v-model:value="title">
+			<span>{{ $ts.title }}</span>
+		</FormInput>
+
+		<FormTextarea v-model:value="description" :max="500">
+			<span>{{ $ts.description }}</span>
+		</FormTextarea>
+
+		<FormGroup>
+			<div v-for="file in files" :key="file.id" class="_formItem _formPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
+				<div class="name">{{ file.name }}</div>
+				<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button>
+			</div>
+			<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
+		</FormGroup>
+
+		<FormSwitch v-model:value="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
+
+		<FormButton v-if="postId" @click="save" primary><i class="fas fa-save"></i> {{ $ts.save }}</FormButton>
+		<FormButton v-else @click="save" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
+
+		<FormButton v-if="postId" @click="del" danger><i class="fas fa-trash-alt"></i> {{ $ts.delete }}</FormButton>
+	</FormSuspense>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent } from 'vue';
+import FormButton from '@client/components/form/button.vue';
+import FormInput from '@client/components/form/input.vue';
+import FormTextarea from '@client/components/form/textarea.vue';
+import FormSwitch from '@client/components/form/switch.vue';
+import FormTuple from '@client/components/form/tuple.vue';
+import FormBase from '@client/components/form/base.vue';
+import FormGroup from '@client/components/form/group.vue';
+import FormSuspense from '@client/components/form/suspense.vue';
+import { selectFile } from '@client/scripts/select-file';
+import * as os from '@client/os';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		FormButton,
+		FormInput,
+		FormTextarea,
+		FormSwitch,
+		FormBase,
+		FormGroup,
+		FormSuspense,
+	},
+
+	props: {
+		postId: {
+			type: String,
+			required: false,
+			default: null,
+		}
+	},
+	
+	data() {
+		return {
+			[symbols.PAGE_INFO]: computed(() => this.postId ? {
+				title: this.$ts.edit,
+				icon: 'fas fa-pencil-alt'
+			} : {
+				title: this.$ts.postToGallery,
+				icon: 'fas fa-pencil-alt'
+			}),
+			init: null,
+			files: [],
+			description: null,
+			title: null,
+			isSensitive: false,
+		}
+	},
+
+	watch: {
+		postId: {
+			handler() {
+				this.init = () => this.postId ? os.api('gallery/posts/show', {
+					postId: this.postId
+				}).then(post => {
+					this.files = post.files;
+					this.title = post.title;
+					this.description = post.description;
+					this.isSensitive = post.isSensitive;
+				}) : Promise.resolve(null);
+			},
+			immediate: true,
+		}
+	},
+
+	methods: {
+		selectFile(e) {
+			selectFile(e.currentTarget || e.target, null, true).then(files => {
+				this.files = this.files.concat(files);
+			});
+		},
+
+		remove(file) {
+			this.files = this.files.filter(f => f.id !== file.id);
+		},
+
+		async save() {
+			if (this.postId) {
+				await os.apiWithDialog('gallery/posts/update', {
+					postId: this.postId,
+					title: this.title,
+					description: this.description,
+					fileIds: this.files.map(file => file.id),
+					isSensitive: this.isSensitive,
+				});
+				this.$router.push(`/gallery/${this.postId}`);
+			} else {
+				const post = await os.apiWithDialog('gallery/posts/create', {
+					title: this.title,
+					description: this.description,
+					fileIds: this.files.map(file => file.id),
+					isSensitive: this.isSensitive,
+				});
+				this.$router.push(`/gallery/${post.id}`);
+			}
+		},
+
+		async del() {
+			const { canceled } = await os.dialog({
+				type: 'warning',
+				text: this.$ts.deleteConfirm,
+				showCancelButton: true
+			});
+			if (canceled) return;
+			await os.apiWithDialog('gallery/posts/delete', {
+				postId: this.postId,
+			});
+			this.$router.push(`/gallery`);
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wqugxsfx {
+	height: 200px;
+	background-size: contain;
+	background-position: center;
+	background-repeat: no-repeat;
+	position: relative;
+
+	> .name {
+		position: absolute;
+		top: 8px;
+		left: 9px;
+		padding: 8px;
+		background: var(--panel);
+	}
+
+	> .remove {
+		position: absolute;
+		top: 8px;
+		right: 9px;
+		padding: 8px;
+		background: var(--panel);
+	}
+}
+</style>
diff --git a/src/client/pages/gallery/new.vue b/src/client/pages/gallery/new.vue
deleted file mode 100644
index 3f9756df8e..0000000000
--- a/src/client/pages/gallery/new.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<template>
-<FormBase>
-	<FormInput v-model:value="title">
-		<span>{{ $ts.title }}</span>
-	</FormInput>
-
-	<FormTextarea v-model:value="description" :max="500">
-		<span>{{ $ts.description }}</span>
-	</FormTextarea>
-
-	<FormGroup>
-		<div v-for="file in files" :key="file.id" class="_formItem _formPanel wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : null }">
-			<div class="name">{{ file.name }}</div>
-			<button class="remove _button" @click="remove(file)" v-tooltip="$ts.remove"><i class="fas fa-times"></i></button>
-		</div>
-		<FormButton @click="selectFile" primary><i class="fas fa-plus"></i> {{ $ts.attachFile }}</FormButton>
-	</FormGroup>
-
-	<FormSwitch v-model:value="isSensitive">{{ $ts.markAsSensitive }}</FormSwitch>
-
-	<FormButton @click="publish" primary><i class="fas fa-save"></i> {{ $ts.publish }}</FormButton>
-</FormBase>
-</template>
-
-<script lang="ts">
-import { defineComponent } from 'vue';
-import FormButton from '@client/components/form/button.vue';
-import FormInput from '@client/components/form/input.vue';
-import FormTextarea from '@client/components/form/textarea.vue';
-import FormSwitch from '@client/components/form/switch.vue';
-import FormTuple from '@client/components/form/tuple.vue';
-import FormBase from '@client/components/form/base.vue';
-import FormGroup from '@client/components/form/group.vue';
-import { selectFile } from '@client/scripts/select-file';
-import * as os from '@client/os';
-import * as symbols from '@client/symbols';
-
-export default defineComponent({
-	components: {
-		FormButton,
-		FormInput,
-		FormTextarea,
-		FormSwitch,
-		FormBase,
-		FormGroup,
-	},
-	
-	data() {
-		return {
-			[symbols.PAGE_INFO]: {
-				title: this.$ts.postToGallery,
-				icon: 'fas fa-pencil-alt'
-			},
-			files: [],
-			description: null,
-			title: null,
-			isSensitive: false,
-		}
-	},
-
-	methods: {
-		selectFile(e) {
-			selectFile(e.currentTarget || e.target, null, true).then(files => {
-				this.files = this.files.concat(files);
-			});
-		},
-
-		remove(file) {
-			this.files = this.files.filter(f => f.id !== file.id);
-		},
-
-		async publish() {
-			const post = await os.apiWithDialog('gallery/posts/create', {
-				title: this.title,
-				description: this.description,
-				fileIds: this.files.map(file => file.id),
-				isSensitive: this.isSensitive,
-			});
-
-			this.$router.push(`/gallery/${post.id}`);
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.wqugxsfx {
-	height: 200px;
-	background-size: contain;
-	background-position: center;
-	background-repeat: no-repeat;
-	position: relative;
-
-	> .name {
-		position: absolute;
-		top: 8px;
-		left: 9px;
-		padding: 8px;
-		background: var(--panel);
-	}
-
-	> .remove {
-		position: absolute;
-		top: 8px;
-		right: 9px;
-		padding: 8px;
-		background: var(--panel);
-	}
-}
-</style>
diff --git a/src/client/pages/gallery/post.vue b/src/client/pages/gallery/post.vue
index 9bd102cee2..703506a78d 100644
--- a/src/client/pages/gallery/post.vue
+++ b/src/client/pages/gallery/post.vue
@@ -19,6 +19,7 @@
 						<MkButton class="button" @click="like()" v-else v-tooltip="$ts._gallery.like"><i class="far fa-heart"></i><span class="count" v-if="post.likedCount > 0">{{ post.likedCount }}</span></MkButton>
 					</div>
 					<div class="other">
+						<button v-if="$i && $i.id === post.user.id" class="_button" @click="edit" v-tooltip="$ts.edit" v-click-anime><i class="fas fa-pencil-alt fa-fw"></i></button>
 						<button class="_button" @click="shareWithNote" v-tooltip="$ts.shareWithNote" v-click-anime><i class="fas fa-retweet fa-fw"></i></button>
 						<button class="_button" @click="share" v-tooltip="$ts.share" v-click-anime><i class="fas fa-share-alt fa-fw"></i></button>
 					</div>
@@ -84,6 +85,11 @@ export default defineComponent({
 					title: this.post.title,
 					text: this.post.description,
 				},
+				actions: [{
+					icon: 'fas fa-pencil-alt',
+					text: this.$ts.edit,
+					handler: this.edit
+				}]
 			} : null),
 			otherPostsPagination: {
 				endpoint: 'users/gallery/posts',
@@ -154,6 +160,10 @@ export default defineComponent({
 				this.post.likedCount--;
 			});
 		},
+
+		edit() {
+			this.$router.push(`/gallery/${this.post.id}/edit`);
+		}
 	}
 });
 </script>
diff --git a/src/client/router.ts b/src/client/router.ts
index 5371bf17d9..8dcc1d1eb4 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -38,7 +38,8 @@ export const router = createRouter({
 		{ path: '/pages/new', component: page('page-editor/page-editor') },
 		{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
 		{ path: '/gallery', component: page('gallery/index') },
-		{ path: '/gallery/new', component: page('gallery/new') },
+		{ path: '/gallery/new', component: page('gallery/edit') },
+		{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
 		{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
 		{ path: '/channels', component: page('channels') },
 		{ path: '/channels/new', component: page('channel-editor') },
diff --git a/src/server/api/endpoints/gallery/posts/delete.ts b/src/server/api/endpoints/gallery/posts/delete.ts
new file mode 100644
index 0000000000..8b54828b20
--- /dev/null
+++ b/src/server/api/endpoints/gallery/posts/delete.ts
@@ -0,0 +1,40 @@
+import $ from 'cafy';
+import define from '../../../define';
+import { ApiError } from '../../../error';
+import { GalleryPosts } from '../../../../../models';
+import { ID } from '@/misc/cafy-id';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery',
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		},
+	},
+
+	errors: {
+		noSuchPost: {
+			message: 'No such post.',
+			code: 'NO_SUCH_POST',
+			id: 'ae52f367-4bd7-4ecd-afc6-5672fff427f5'
+		},
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const post = await GalleryPosts.findOne({
+		id: ps.postId,
+		userId: user.id,
+	});
+
+	if (post == null) {
+		throw new ApiError(meta.errors.noSuchPost);
+	}
+
+	await GalleryPosts.delete(post.id);
+});
diff --git a/src/server/api/endpoints/gallery/posts/update.ts b/src/server/api/endpoints/gallery/posts/update.ts
new file mode 100644
index 0000000000..c8bb8d48c9
--- /dev/null
+++ b/src/server/api/endpoints/gallery/posts/update.ts
@@ -0,0 +1,81 @@
+import $ from 'cafy';
+import * as ms from 'ms';
+import define from '../../../define';
+import { ID } from '../../../../../misc/cafy-id';
+import { DriveFiles, GalleryPosts } from '../../../../../models';
+import { GalleryPost } from '../../../../../models/entities/gallery-post';
+import { ApiError } from '../../../error';
+
+export const meta = {
+	tags: ['gallery'],
+
+	requireCredential: true as const,
+
+	kind: 'write:gallery',
+
+	limit: {
+		duration: ms('1hour'),
+		max: 300
+	},
+
+	params: {
+		postId: {
+			validator: $.type(ID),
+		},
+
+		title: {
+			validator: $.str.min(1),
+		},
+
+		description: {
+			validator: $.optional.nullable.str,
+		},
+
+		fileIds: {
+			validator: $.arr($.type(ID)).unique().range(1, 32),
+		},
+
+		isSensitive: {
+			validator: $.optional.bool,
+			default: false,
+		},
+	},
+
+	res: {
+		type: 'object' as const,
+		optional: false as const, nullable: false as const,
+		ref: 'GalleryPost',
+	},
+
+	errors: {
+
+	}
+};
+
+export default define(meta, async (ps, user) => {
+	const files = (await Promise.all(ps.fileIds.map(fileId =>
+		DriveFiles.findOne({
+			id: fileId,
+			userId: user.id
+		})
+	))).filter(file => file != null);
+
+	if (files.length === 0) {
+		throw new Error();
+	}
+
+	await GalleryPosts.update({
+		id: ps.postId,
+		userId: user.id,
+	}, {
+		updatedAt: new Date(),
+		title: ps.title,
+		description: ps.description,
+		isSensitive: ps.isSensitive,
+		fileIds: files.map(file => file.id)
+	});
+
+	const post = await GalleryPosts.findOneOrFail(ps.postId);
+
+	return await GalleryPosts.pack(post, user);
+});