summaryrefslogtreecommitdiff
path: root/internal/federation/dereferencing/media.go
blob: 956866e9498f4dc8d097c0eaf5cd718c6bad591c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package dereferencing

import (
	"context"
	"io"
	"net/url"

	"github.com/superseriousbusiness/gotosocial/internal/config"
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
	"github.com/superseriousbusiness/gotosocial/internal/media"
)

// GetMedia fetches the media at given remote URL by
// dereferencing it. The passed accountID is used to
// store it as being owned by that account. Additional
// information to set on the media attachment may also
// be provided.
//
// Please note that even if an error is returned,
// a media model may still be returned if the error
// was only encountered during actual dereferencing.
// In this case, it will act as a placeholder.
//
// Also note that since account / status dereferencing is
// already protected by per-uri locks, and that fediverse
// media is generally not shared between accounts (etc),
// there aren't any concurrency protections against multiple
// insertion / dereferencing of media at remoteURL. Worst
// case scenario, an extra media entry will be inserted
// and the scheduled cleaner.Cleaner{} will catch it!
func (d *Dereferencer) GetMedia(
	ctx context.Context,
	requestUser string,
	accountID string, // media account owner
	remoteURL string,
	info media.AdditionalMediaInfo,
) (
	*gtsmodel.MediaAttachment,
	error,
) {
	// Parse str as valid URL object.
	url, err := url.Parse(remoteURL)
	if err != nil {
		return nil, gtserror.Newf("invalid remote media url %q: %v", remoteURL, err)
	}

	// Fetch transport for the provided request user from controller.
	tsport, err := d.transportController.NewTransportForUsername(ctx,
		requestUser,
	)
	if err != nil {
		return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
	}

	// Get maximum supported remote media size.
	maxsz := config.GetMediaRemoteMaxSize()

	// Start processing remote attachment at URL.
	processing, err := d.mediaManager.CreateMedia(
		ctx,
		accountID,
		func(ctx context.Context) (io.ReadCloser, error) {
			return tsport.DereferenceMedia(ctx, url, int64(maxsz))
		},
		info,
	)
	if err != nil {
		return nil, err
	}

	// Perform media load operation.
	media, err := processing.Load(ctx)
	if err != nil {
		err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)

		// TODO: in time we should return checkable flags by gtserror.Is___()
		// which can determine if loading error should allow remaining placeholder.
	}

	return media, err
}

// RefreshMedia ensures that given media is up-to-date,
// both in terms of being cached in local instance,
// storage and compared to extra info in information
// in given gtsmodel.AdditionMediaInfo{}. This handles
// the case of local emoji by returning early.
//
// Please note that even if an error is returned,
// a media model may still be returned if the error
// was only encountered during actual dereferencing.
// In this case, it will act as a placeholder.
//
// Also note that since account / status dereferencing is
// already protected by per-uri locks, and that fediverse
// media is generally not shared between accounts (etc),
// there aren't any concurrency protections against multiple
// insertion / dereferencing of media at remoteURL. Worst
// case scenario, an extra media entry will be inserted
// and the scheduled cleaner.Cleaner{} will catch it!
func (d *Dereferencer) RefreshMedia(
	ctx context.Context,
	requestUser string,
	media *gtsmodel.MediaAttachment,
	info media.AdditionalMediaInfo,
	force bool,
) (
	*gtsmodel.MediaAttachment,
	error,
) {
	// Can't refresh local.
	if media.IsLocal() {
		return media, nil
	}

	// Check emoji is up-to-date
	// with provided extra info.
	switch {
	case info.Blurhash != nil &&
		*info.Blurhash != media.Blurhash:
		force = true
	case info.Description != nil &&
		*info.Description != media.Description:
		force = true
	case info.RemoteURL != nil &&
		*info.RemoteURL != media.RemoteURL:
		force = true
	}

	// Check if needs updating.
	if !force && *media.Cached {
		return media, nil
	}

	// TODO: more finegrained freshness checks.

	// Ensure we have a valid remote URL.
	url, err := url.Parse(media.RemoteURL)
	if err != nil {
		err := gtserror.Newf("invalid media remote url %s: %w", media.RemoteURL, err)
		return nil, err
	}

	// Fetch transport for the provided request user from controller.
	tsport, err := d.transportController.NewTransportForUsername(ctx,
		requestUser,
	)
	if err != nil {
		return nil, gtserror.Newf("failed getting transport for %s: %w", requestUser, err)
	}

	// Get maximum supported remote media size.
	maxsz := config.GetMediaRemoteMaxSize()

	// Start processing remote attachment recache.
	processing := d.mediaManager.RecacheMedia(
		media,
		func(ctx context.Context) (io.ReadCloser, error) {
			return tsport.DereferenceMedia(ctx, url, int64(maxsz))
		},
	)

	// Perform media load operation.
	media, err = processing.Load(ctx)
	if err != nil {
		err = gtserror.Newf("error loading media %s: %w", media.RemoteURL, err)

		// TODO: in time we should return checkable flags by gtserror.Is___()
		// which can determine if loading error should allow remaining placeholder.
	}

	return media, err
}

// updateAttachment handles the case of an existing media attachment
// that *may* have changes or need recaching. it checks for changed
// fields, updating in the database if so, and recaches uncached media.
func (d *Dereferencer) updateAttachment(
	ctx context.Context,
	requestUser string,
	existing *gtsmodel.MediaAttachment, // existing attachment
	attach *gtsmodel.MediaAttachment, // (optional) changed media
) (
	*gtsmodel.MediaAttachment, // always set
	error,
) {
	var info media.AdditionalMediaInfo

	if attach != nil {
		// Set optional extra information,
		// (will later check for changes).
		info.Description = &attach.Description
		info.Blurhash = &attach.Blurhash
		info.RemoteURL = &attach.RemoteURL
	}

	// Ensure media is cached.
	return d.RefreshMedia(ctx,
		requestUser,
		existing,
		info,
		false,
	)
}