Track usage of media files and add a job to clean them

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-11-26 11:41:13 +01:00
parent c19e326bd8
commit c9457fe0d3
78 changed files with 1405 additions and 700 deletions

View File

@@ -212,7 +212,7 @@ import { SEARCH_PERSONS } from "../graphql/search";
import { Actor, IActor, IPerson } from "../types/actor";
import Image from "./Editor/Image";
import MaxSize from "./Editor/MaxSize";
import { UPLOAD_PICTURE } from "../graphql/upload";
import { UPLOAD_MEDIA } from "../graphql/upload";
import { listenFileUpload } from "../utils/upload";
import { CURRENT_ACTOR_CLIENT } from "../graphql/actor";
import { IComment } from "../types/comment.model";
@@ -395,7 +395,15 @@ export default class EditorComponent extends Vue {
new Image(),
new MaxSize({ maxSize: this.maxSize }),
],
onUpdate: ({ getHTML }: { getHTML: Function }) => {
onUpdate: ({
getHTML,
transaction,
getJSON,
}: {
getHTML: Function;
getJSON: Function;
transaction: unknown;
}) => {
this.$emit("input", getHTML());
},
});
@@ -526,14 +534,14 @@ export default class EditorComponent extends Vue {
const image = await listenFileUpload();
try {
const { data } = await this.$apollo.mutate({
mutation: UPLOAD_PICTURE,
mutation: UPLOAD_MEDIA,
variables: {
file: image,
name: image.name,
},
});
if (data.uploadPicture && data.uploadPicture.url) {
command({ src: data.uploadPicture.url });
if (data.uploadMedia && data.uploadMedia.url) {
command({ src: data.uploadMedia.url, "data-media-id": data.uploadMedia.id });
}
} catch (error) {
console.error(error);

View File

@@ -1,6 +1,7 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { Node } from "tiptap";
import { UPLOAD_PICTURE } from "@/graphql/upload";
import { UPLOAD_MEDIA } from "@/graphql/upload";
import apolloProvider from "@/vue-apollo";
import ApolloClient from "apollo-client";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
@@ -27,16 +28,18 @@ export default class Image extends Node {
title: {
default: null,
},
"data-media-id": {},
},
group: "inline",
draggable: true,
parseDOM: [
{
tag: "img[src]",
tag: "img",
getAttrs: (dom: any) => ({
src: dom.getAttribute("src"),
title: dom.getAttribute("title"),
alt: dom.getAttribute("alt"),
"data-media-id": dom.getAttribute("data-media-id"),
}),
},
],
@@ -92,13 +95,16 @@ export default class Image extends Node {
try {
images.forEach(async (image) => {
const { data } = await client.mutate({
mutation: UPLOAD_PICTURE,
mutation: UPLOAD_MEDIA,
variables: {
file: image,
name: image.name,
},
});
const node = schema.nodes.image.create({ src: data.uploadPicture.url });
const node = schema.nodes.image.create({
src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id,
});
const transaction = view.state.tr.insert(coordinates.pos, node);
view.dispatch(transaction);
});

View File

@@ -60,14 +60,14 @@ figure.image {
</style>
<script lang="ts">
import { IPicture } from "@/types/picture.model";
import { IMedia } from "@/types/media.model";
import { Component, Model, Prop, Vue, Watch } from "vue-property-decorator";
@Component
export default class PictureUpload extends Vue {
@Model("change", { type: File }) readonly pictureFile!: File;
@Prop({ type: Object, required: false }) defaultImage!: IPicture;
@Prop({ type: Object, required: false }) defaultImage!: IMedia;
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
accept!: string;
@@ -100,7 +100,7 @@ export default class PictureUpload extends Vue {
}
@Watch("defaultImage")
onDefaultImageChange(defaultImage: IPicture): void {
onDefaultImageChange(defaultImage: IMedia): void {
console.log("onDefaultImageChange", defaultImage);
this.imageSrc = defaultImage ? defaultImage.url : null;
}

View File

@@ -421,7 +421,7 @@ export const CREATE_PERSON = gql`
$preferredUsername: String!
$name: String!
$summary: String
$avatar: PictureInput
$avatar: MediaInput
) {
createPerson(
preferredUsername: $preferredUsername
@@ -442,7 +442,7 @@ export const CREATE_PERSON = gql`
`;
export const UPDATE_PERSON = gql`
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: PictureInput) {
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: MediaInput) {
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
id
preferredUsername

View File

@@ -244,7 +244,7 @@ export const CREATE_EVENT = gql`
$joinOptions: EventJoinOptions,
$draft: Boolean,
$tags: [String],
$picture: PictureInput,
$picture: MediaInput,
$onlineAddress: String,
$phoneAddress: String,
$category: String,
@@ -355,7 +355,7 @@ export const EDIT_EVENT = gql`
$joinOptions: EventJoinOptions,
$draft: Boolean,
$tags: [String],
$picture: PictureInput,
$picture: MediaInput,
$onlineAddress: String,
$phoneAddress: String,
$organizerActorId: ID,

View File

@@ -227,8 +227,8 @@ export const CREATE_GROUP = gql`
$preferredUsername: String!
$name: String!
$summary: String
$avatar: PictureInput
$banner: PictureInput
$avatar: MediaInput
$banner: MediaInput
) {
createGroup(
preferredUsername: $preferredUsername
@@ -259,8 +259,8 @@ export const UPDATE_GROUP = gql`
$id: ID!
$name: String
$summary: String
$avatar: PictureInput
$banner: PictureInput
$avatar: MediaInput
$banner: MediaInput
$visibility: GroupVisibility
$openness: Openness
$physicalAddress: AddressInput

View File

@@ -119,7 +119,7 @@ export const CREATE_POST = gql`
$visibility: PostVisibility
$draft: Boolean
$tags: [String]
$picture: PictureInput
$picture: MediaInput
) {
createPost(
title: $title
@@ -145,7 +145,7 @@ export const UPDATE_POST = gql`
$visibility: PostVisibility
$draft: Boolean
$tags: [String]
$picture: PictureInput
$picture: MediaInput
) {
updatePost(
id: $id

View File

@@ -1,17 +1,17 @@
import gql from "graphql-tag";
export const UPLOAD_PICTURE = gql`
mutation UploadPicture($file: Upload!, $alt: String, $name: String!) {
uploadPicture(file: $file, alt: $alt, name: $name) {
export const UPLOAD_MEDIA = gql`
mutation UploadMedia($file: Upload!, $alt: String, $name: String!) {
uploadMedia(file: $file, alt: $alt, name: $name) {
url
id
}
}
`;
export const REMOVE_PICTURE = gql`
mutation RemovePicture($id: ID!) {
removePicture(id: $id) {
export const REMOVE_MEDIA = gql`
mutation RemoveMedia($id: ID!) {
removeMedia(id: $id) {
id
}
}

View File

@@ -1,4 +1,4 @@
import { IPicture } from "@/types/picture.model";
import { IMedia } from "@/types/media.model";
export enum ActorType {
PERSON = "PERSON",
@@ -17,17 +17,17 @@ export interface IActor {
summary: string;
preferredUsername: string;
suspended: boolean;
avatar?: IPicture | null;
banner?: IPicture | null;
avatar?: IMedia | null;
banner?: IMedia | null;
type: ActorType;
}
export class Actor implements IActor {
id?: string;
avatar: IPicture | null = null;
avatar: IMedia | null = null;
banner: IPicture | null = null;
banner: IMedia | null = null;
domain: string | null = null;

View File

@@ -1,6 +1,6 @@
import { Address, IAddress } from "@/types/address.model";
import { ITag } from "@/types/tag.model";
import { IPicture } from "@/types/picture.model";
import { IMedia } from "@/types/media.model";
import { IComment } from "@/types/comment.model";
import { Paginate } from "@/types/paginate";
import { Actor, Group, IActor, IGroup, IPerson } from "./actor";
@@ -69,7 +69,7 @@ interface IEventEditJSON {
visibility: EventVisibility;
joinOptions: EventJoinOptions;
draft: boolean;
picture?: IPicture | { pictureId: string } | null;
picture?: IMedia | { mediaId: string } | null;
attributedToId: string | null;
onlineAddress?: string;
phoneAddress?: string;
@@ -96,7 +96,7 @@ export interface IEvent {
joinOptions: EventJoinOptions;
draft: boolean;
picture: IPicture | null;
picture: IMedia | null;
organizerActor?: IActor;
attributedTo?: IGroup;
@@ -142,7 +142,7 @@ export class EventModel implements IEvent {
physicalAddress?: IAddress;
picture: IPicture | null = null;
picture: IMedia | null = null;
visibility = EventVisibility.PUBLIC;

View File

@@ -1,11 +1,11 @@
export interface IPicture {
export interface IMedia {
id: string;
url: string;
name: string;
alt: string;
}
export interface IPictureUpload {
export interface IMediaUpload {
file: File;
name: string;
alt: string | null;

View File

@@ -1,5 +1,5 @@
import { ITag } from "./tag.model";
import { IPicture } from "./picture.model";
import { IMedia } from "./media.model";
import { IActor } from "./actor";
export enum PostVisibility {
@@ -17,7 +17,7 @@ export interface IPost {
title: string;
body: string;
tags?: ITag[];
picture?: IPicture | null;
picture?: IMedia | null;
draft: boolean;
visibility: PostVisibility;
author?: IActor;

View File

@@ -1,6 +1,6 @@
import { IPicture } from "@/types/picture.model";
import { IMedia } from "@/types/media.model";
export async function buildFileFromIPicture(obj: IPicture | null | undefined): Promise<File | null> {
export async function buildFileFromIMedia(obj: IMedia | null | undefined): Promise<File | null> {
if (!obj) return Promise.resolve(null);
const response = await fetch(obj.url);
@@ -14,7 +14,7 @@ export function buildFileVariable(file: File | null, name: string, alt?: string)
return {
[name]: {
picture: {
media: {
name: file.name,
alt: alt || file.name,
file,

View File

@@ -124,7 +124,6 @@ h1 {
<script lang="ts">
import { Component, Prop, Watch } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import { IPicture } from "@/types/picture.model";
import {
CREATE_PERSON,
CURRENT_ACTOR_CLIENT,
@@ -137,7 +136,7 @@ import { IPerson, Person } from "../../../types/actor";
import PictureUpload from "../../../components/PictureUpload.vue";
import { MOBILIZON_INSTANCE_HOST } from "../../../api/_entrypoint";
import RouteName from "../../../router/name";
import { buildFileFromIPicture, buildFileVariable } from "../../../utils/image";
import { buildFileVariable } from "../../../utils/image";
import { changeIdentity } from "../../../utils/auth";
import identityEditionMixin from "../../../mixins/identityEdition";

View File

@@ -377,7 +377,7 @@ import {
import { IPerson, Person, displayNameAndUsername } from "../../types/actor";
import { TAGS } from "../../graphql/tags";
import { ITag } from "../../types/tag.model";
import { buildFileFromIPicture, buildFileVariable, readFileAsync } from "../../utils/image";
import { buildFileFromIMedia, buildFileVariable, readFileAsync } from "../../utils/image";
import RouteName from "../../router/name";
import "intersection-observer";
import { CONFIG } from "../../graphql/config";
@@ -517,7 +517,7 @@ export default class EditEvent extends Vue {
);
this.observer.observe(this.$refs.bottomObserver as Element);
this.pictureFile = await buildFileFromIPicture(this.event.picture);
this.pictureFile = await buildFileFromIMedia(this.event.picture);
this.limitedPlaces = this.event.options.maximumAttendeeCapacity > 0;
if (!(this.isUpdate || this.isDuplicate)) {
this.initializeEvent();
@@ -775,11 +775,11 @@ export default class EditEvent extends Vue {
try {
if (this.event.picture && this.pictureFile) {
const oldPictureFile = (await buildFileFromIPicture(this.event.picture)) as File;
const oldPictureFile = (await buildFileFromIMedia(this.event.picture)) as File;
const oldPictureFileContent = await readFileAsync(oldPictureFile);
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
if (oldPictureFileContent === newPictureFileContent) {
res.picture = { pictureId: this.event.picture.id };
res.picture = { mediaId: this.event.picture.id };
}
}
} catch (e) {

View File

@@ -246,7 +246,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
if (this.avatarFile) {
avatarObj = {
avatar: {
picture: {
media: {
name: this.avatarFile.name,
alt: `${this.group.preferredUsername}'s avatar`,
file: this.avatarFile,
@@ -258,7 +258,7 @@ export default class GroupSettings extends mixins(GroupMixin) {
if (this.bannerFile) {
bannerObj = {
banner: {
picture: {
media: {
name: this.bannerFile.name,
alt: `${this.group.preferredUsername}'s banner`,
file: this.bannerFile,

View File

@@ -103,7 +103,7 @@
import { Component, Prop } from "vue-property-decorator";
import { mixins } from "vue-class-component";
import { FETCH_GROUP } from "@/graphql/group";
import { buildFileFromIPicture, readFileAsync } from "@/utils/image";
import { buildFileFromIMedia, readFileAsync } from "@/utils/image";
import GroupMixin from "@/mixins/group";
import { TAGS } from "../../graphql/tags";
import { CONFIG } from "../../graphql/config";
@@ -188,7 +188,7 @@ export default class EditPost extends mixins(GroupMixin) {
errors: Record<string, unknown> = {};
async mounted(): Promise<void> {
this.pictureFile = await buildFileFromIPicture(this.post.picture);
this.pictureFile = await buildFileFromIMedia(this.post.picture);
}
// eslint-disable-next-line consistent-return
@@ -277,11 +277,11 @@ export default class EditPost extends mixins(GroupMixin) {
}
try {
if (this.post.picture) {
const oldPictureFile = (await buildFileFromIPicture(this.post.picture)) as File;
const oldPictureFile = (await buildFileFromIMedia(this.post.picture)) as File;
const oldPictureFileContent = await readFileAsync(oldPictureFile);
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
if (oldPictureFileContent === newPictureFileContent) {
obj.picture = { pictureId: this.post.picture.id };
obj.picture = { mediaId: this.post.picture.id };
}
}
} catch (e) {