aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-01-06 23:24:47 +0100
committerChocobozzz <florian.bigard@gmail.com>2017-01-06 23:46:36 +0100
commited04d94f6d7132055f97a2f757b85c03c5f2a0b6 (patch)
tree2f1f8c6f6e46b9ab18ba009403f80b14af61af68
parentbb0b243c92577872a5f4d98f707e078082af4d2a (diff)
downloadPeerTube-ed04d94f6d7132055f97a2f757b85c03c5f2a0b6.tar.gz
PeerTube-ed04d94f6d7132055f97a2f757b85c03c5f2a0b6.tar.zst
PeerTube-ed04d94f6d7132055f97a2f757b85c03c5f2a0b6.zip
Server: try to have a better video integrity
-rw-r--r--server/controllers/api/remote/videos.js56
-rw-r--r--server/controllers/api/videos.js114
-rw-r--r--server/helpers/utils.js16
-rw-r--r--server/lib/friends.js66
-rw-r--r--server/models/pod.js9
-rw-r--r--server/models/request.js4
6 files changed, 194 insertions, 71 deletions
diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js
index d02da4463..6d768eae8 100644
--- a/server/controllers/api/remote/videos.js
+++ b/server/controllers/api/remote/videos.js
@@ -10,6 +10,7 @@ const secureMiddleware = middlewares.secure
10const videosValidators = middlewares.validators.remote.videos 10const videosValidators = middlewares.validators.remote.videos
11const signatureValidators = middlewares.validators.remote.signature 11const signatureValidators = middlewares.validators.remote.signature
12const logger = require('../../../helpers/logger') 12const logger = require('../../../helpers/logger')
13const utils = require('../../../helpers/utils')
13 14
14const router = express.Router() 15const router = express.Router()
15 16
@@ -37,11 +38,11 @@ function remoteVideos (req, res, next) {
37 38
38 switch (request.type) { 39 switch (request.type) {
39 case 'add': 40 case 'add':
40 addRemoteVideo(data, fromPod, callbackEach) 41 addRemoteVideoRetryWrapper(data, fromPod, callbackEach)
41 break 42 break
42 43
43 case 'update': 44 case 'update':
44 updateRemoteVideo(data, fromPod, callbackEach) 45 updateRemoteVideoRetryWrapper(data, fromPod, callbackEach)
45 break 46 break
46 47
47 case 'remove': 48 case 'remove':
@@ -63,13 +64,30 @@ function remoteVideos (req, res, next) {
63 return res.type('json').status(204).end() 64 return res.type('json').status(204).end()
64} 65}
65 66
67// Handle retries on fail
68function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
69 utils.transactionRetryer(
70 function (callback) {
71 return addRemoteVideo(videoToCreateData, fromPod, callback)
72 },
73 function (err) {
74 if (err) {
75 logger.error('Cannot insert the remote video with many retries.', { error: err })
76 return finalCallback(err)
77 }
78
79 return finalCallback()
80 }
81 )
82}
83
66function addRemoteVideo (videoToCreateData, fromPod, finalCallback) { 84function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
67 logger.debug('Adding remote video "%s".', videoToCreateData.name) 85 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
68 86
69 waterfall([ 87 waterfall([
70 88
71 function startTransaction (callback) { 89 function startTransaction (callback) {
72 db.sequelize.transaction().asCallback(function (err, t) { 90 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
73 return callback(err, t) 91 return callback(err, t)
74 }) 92 })
75 }, 93 },
@@ -103,6 +121,7 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
103 authorId: author.id, 121 authorId: author.id,
104 duration: videoToCreateData.duration, 122 duration: videoToCreateData.duration,
105 createdAt: videoToCreateData.createdAt, 123 createdAt: videoToCreateData.createdAt,
124 // FIXME: updatedAt does not seems to be considered by Sequelize
106 updatedAt: videoToCreateData.updatedAt 125 updatedAt: videoToCreateData.updatedAt
107 } 126 }
108 127
@@ -142,7 +161,8 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
142 161
143 ], function (err, t) { 162 ], function (err, t) {
144 if (err) { 163 if (err) {
145 logger.error('Cannot insert the remote video.') 164 // This is just a debug because we will retry the insert
165 logger.debug('Cannot insert the remote video.', { error: err })
146 166
147 // Abort transaction? 167 // Abort transaction?
148 if (t) t.rollback() 168 if (t) t.rollback()
@@ -157,8 +177,25 @@ function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
157 }) 177 })
158} 178}
159 179
180// Handle retries on fail
181function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
182 utils.transactionRetryer(
183 function (callback) {
184 return updateRemoteVideo(videoAttributesToUpdate, fromPod, callback)
185 },
186 function (err) {
187 if (err) {
188 logger.error('Cannot update the remote video with many retries.', { error: err })
189 return finalCallback(err)
190 }
191
192 return finalCallback()
193 }
194 )
195}
196
160function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) { 197function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
161 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.name) 198 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
162 199
163 waterfall([ 200 waterfall([
164 201
@@ -208,7 +245,8 @@ function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
208 245
209 ], function (err, t) { 246 ], function (err, t) {
210 if (err) { 247 if (err) {
211 logger.error('Cannot update the remote video.') 248 // This is just a debug because we will retry the insert
249 logger.debug('Cannot update the remote video.', { error: err })
212 250
213 // Abort transaction? 251 // Abort transaction?
214 if (t) t.rollback() 252 if (t) t.rollback()
@@ -238,7 +276,7 @@ function reportAbuseRemoteVideo (reportData, fromPod, callback) {
238 if (err || !video) { 276 if (err || !video) {
239 if (!err) err = new Error('video not found') 277 if (!err) err = new Error('video not found')
240 278
241 logger.error('Cannot load video from host and remote id.', { error: err }) 279 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
242 return callback(err) 280 return callback(err)
243 } 281 }
244 282
@@ -260,7 +298,7 @@ function fetchVideo (podHost, remoteId, callback) {
260 if (err || !video) { 298 if (err || !video) {
261 if (!err) err = new Error('video not found') 299 if (!err) err = new Error('video not found')
262 300
263 logger.error('Cannot load video from host and remote id.', { error: err }) 301 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
264 return callback(err) 302 return callback(err)
265 } 303 }
266 304
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js
index 6829804ec..4d45c11c0 100644
--- a/server/controllers/api/videos.js
+++ b/server/controllers/api/videos.js
@@ -70,13 +70,13 @@ router.put('/:id',
70 oAuth.authenticate, 70 oAuth.authenticate,
71 reqFiles, 71 reqFiles,
72 validatorsVideos.videosUpdate, 72 validatorsVideos.videosUpdate,
73 updateVideo 73 updateVideoRetryWrapper
74) 74)
75router.post('/', 75router.post('/',
76 oAuth.authenticate, 76 oAuth.authenticate,
77 reqFiles, 77 reqFiles,
78 validatorsVideos.videosAdd, 78 validatorsVideos.videosAdd,
79 addVideo 79 addVideoRetryWrapper
80) 80)
81router.get('/:id', 81router.get('/:id',
82 validatorsVideos.videosGet, 82 validatorsVideos.videosGet,
@@ -103,19 +103,37 @@ module.exports = router
103 103
104// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
105 105
106function addVideo (req, res, next) { 106// Wrapper to video add that retry the function if there is a database error
107 const videoFile = req.files.videofile[0] 107// We need this because we run the transaction in SERIALIZABLE isolation that can fail
108function addVideoRetryWrapper (req, res, next) {
109 utils.transactionRetryer(
110 function (callback) {
111 return addVideo(req, res, req.files.videofile[0], callback)
112 },
113 function (err) {
114 if (err) {
115 logger.error('Cannot insert the video with many retries.', { error: err })
116 return next(err)
117 }
118
119 // TODO : include Location of the new video -> 201
120 return res.type('json').status(204).end()
121 }
122 )
123}
124
125function addVideo (req, res, videoFile, callback) {
108 const videoInfos = req.body 126 const videoInfos = req.body
109 127
110 waterfall([ 128 waterfall([
111 129
112 function startTransaction (callback) { 130 function startTransaction (callbackWaterfall) {
113 db.sequelize.transaction().asCallback(function (err, t) { 131 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
114 return callback(err, t) 132 return callbackWaterfall(err, t)
115 }) 133 })
116 }, 134 },
117 135
118 function findOrCreateAuthor (t, callback) { 136 function findOrCreateAuthor (t, callbackWaterfall) {
119 const user = res.locals.oauth.token.User 137 const user = res.locals.oauth.token.User
120 138
121 const name = user.username 139 const name = user.username
@@ -124,19 +142,19 @@ function addVideo (req, res, next) {
124 const userId = user.id 142 const userId = user.id
125 143
126 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) { 144 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
127 return callback(err, t, authorInstance) 145 return callbackWaterfall(err, t, authorInstance)
128 }) 146 })
129 }, 147 },
130 148
131 function findOrCreateTags (t, author, callback) { 149 function findOrCreateTags (t, author, callbackWaterfall) {
132 const tags = videoInfos.tags 150 const tags = videoInfos.tags
133 151
134 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) { 152 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
135 return callback(err, t, author, tagInstances) 153 return callbackWaterfall(err, t, author, tagInstances)
136 }) 154 })
137 }, 155 },
138 156
139 function createVideoObject (t, author, tagInstances, callback) { 157 function createVideoObject (t, author, tagInstances, callbackWaterfall) {
140 const videoData = { 158 const videoData = {
141 name: videoInfos.name, 159 name: videoInfos.name,
142 remoteId: null, 160 remoteId: null,
@@ -148,74 +166,97 @@ function addVideo (req, res, next) {
148 166
149 const video = db.Video.build(videoData) 167 const video = db.Video.build(videoData)
150 168
151 return callback(null, t, author, tagInstances, video) 169 return callbackWaterfall(null, t, author, tagInstances, video)
152 }, 170 },
153 171
154 // Set the videoname the same as the id 172 // Set the videoname the same as the id
155 function renameVideoFile (t, author, tagInstances, video, callback) { 173 function renameVideoFile (t, author, tagInstances, video, callbackWaterfall) {
156 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR 174 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
157 const source = path.join(videoDir, videoFile.filename) 175 const source = path.join(videoDir, videoFile.filename)
158 const destination = path.join(videoDir, video.getVideoFilename()) 176 const destination = path.join(videoDir, video.getVideoFilename())
159 177
160 fs.rename(source, destination, function (err) { 178 fs.rename(source, destination, function (err) {
161 return callback(err, t, author, tagInstances, video) 179 if (err) return callbackWaterfall(err)
180
181 // This is important in case if there is another attempt
182 videoFile.filename = video.getVideoFilename()
183 return callbackWaterfall(null, t, author, tagInstances, video)
162 }) 184 })
163 }, 185 },
164 186
165 function insertVideoIntoDB (t, author, tagInstances, video, callback) { 187 function insertVideoIntoDB (t, author, tagInstances, video, callbackWaterfall) {
166 const options = { transaction: t } 188 const options = { transaction: t }
167 189
168 // Add tags association 190 // Add tags association
169 video.save(options).asCallback(function (err, videoCreated) { 191 video.save(options).asCallback(function (err, videoCreated) {
170 if (err) return callback(err) 192 if (err) return callbackWaterfall(err)
171 193
172 // Do not forget to add Author informations to the created video 194 // Do not forget to add Author informations to the created video
173 videoCreated.Author = author 195 videoCreated.Author = author
174 196
175 return callback(err, t, tagInstances, videoCreated) 197 return callbackWaterfall(err, t, tagInstances, videoCreated)
176 }) 198 })
177 }, 199 },
178 200
179 function associateTagsToVideo (t, tagInstances, video, callback) { 201 function associateTagsToVideo (t, tagInstances, video, callbackWaterfall) {
180 const options = { transaction: t } 202 const options = { transaction: t }
181 203
182 video.setTags(tagInstances, options).asCallback(function (err) { 204 video.setTags(tagInstances, options).asCallback(function (err) {
183 video.Tags = tagInstances 205 video.Tags = tagInstances
184 206
185 return callback(err, t, video) 207 return callbackWaterfall(err, t, video)
186 }) 208 })
187 }, 209 },
188 210
189 function sendToFriends (t, video, callback) { 211 function sendToFriends (t, video, callbackWaterfall) {
190 video.toAddRemoteJSON(function (err, remoteVideo) { 212 video.toAddRemoteJSON(function (err, remoteVideo) {
191 if (err) return callback(err) 213 if (err) return callbackWaterfall(err)
192 214
193 // Now we'll add the video's meta data to our friends 215 // Now we'll add the video's meta data to our friends
194 friends.addVideoToFriends(remoteVideo) 216 friends.addVideoToFriends(remoteVideo, t, function (err) {
195 217 return callbackWaterfall(err, t)
196 return callback(null, t) 218 })
197 }) 219 })
198 } 220 }
199 221
200 ], function andFinally (err, t) { 222 ], function andFinally (err, t) {
201 if (err) { 223 if (err) {
202 logger.error('Cannot insert the video.') 224 // This is just a debug because we will retry the insert
225 logger.debug('Cannot insert the video.', { error: err })
203 226
204 // Abort transaction? 227 // Abort transaction?
205 if (t) t.rollback() 228 if (t) t.rollback()
206 229
207 return next(err) 230 return callback(err)
208 } 231 }
209 232
210 // Commit transaction 233 // Commit transaction
211 t.commit() 234 t.commit()
212 235
213 // TODO : include Location of the new video -> 201 236 logger.info('Video with name %s created.', videoInfos.name)
214 return res.type('json').status(204).end() 237
238 return callback(null)
215 }) 239 })
216} 240}
217 241
218function updateVideo (req, res, next) { 242function updateVideoRetryWrapper (req, res, next) {
243 utils.transactionRetryer(
244 function (callback) {
245 return updateVideo(req, res, callback)
246 },
247 function (err) {
248 if (err) {
249 logger.error('Cannot update the video with many retries.', { error: err })
250 return next(err)
251 }
252
253 // TODO : include Location of the new video -> 201
254 return res.type('json').status(204).end()
255 }
256 )
257}
258
259function updateVideo (req, res, finalCallback) {
219 const videoInstance = res.locals.video 260 const videoInstance = res.locals.video
220 const videoInfosToUpdate = req.body 261 const videoInfosToUpdate = req.body
221 262
@@ -267,26 +308,25 @@ function updateVideo (req, res, next) {
267 const json = videoInstance.toUpdateRemoteJSON() 308 const json = videoInstance.toUpdateRemoteJSON()
268 309
269 // Now we'll update the video's meta data to our friends 310 // Now we'll update the video's meta data to our friends
270 friends.updateVideoToFriends(json) 311 friends.updateVideoToFriends(json, t, function (err) {
271 312 return callback(err, t)
272 return callback(null, t) 313 })
273 } 314 }
274 315
275 ], function andFinally (err, t) { 316 ], function andFinally (err, t) {
276 if (err) { 317 if (err) {
277 logger.error('Cannot insert the video.') 318 logger.debug('Cannot update the video.', { error: err })
278 319
279 // Abort transaction? 320 // Abort transaction?
280 if (t) t.rollback() 321 if (t) t.rollback()
281 322
282 return next(err) 323 return finalCallback(err)
283 } 324 }
284 325
285 // Commit transaction 326 // Commit transaction
286 t.commit() 327 t.commit()
287 328
288 // TODO : include Location of the new video -> 201 329 return finalCallback(null)
289 return res.type('json').status(204).end()
290 }) 330 })
291} 331}
292 332
diff --git a/server/helpers/utils.js b/server/helpers/utils.js
index 9f4b14582..a902850cd 100644
--- a/server/helpers/utils.js
+++ b/server/helpers/utils.js
@@ -1,6 +1,7 @@
1'use strict' 1'use strict'
2 2
3const crypto = require('crypto') 3const crypto = require('crypto')
4const retry = require('async/retry')
4 5
5const logger = require('./logger') 6const logger = require('./logger')
6 7
@@ -9,7 +10,8 @@ const utils = {
9 cleanForExit, 10 cleanForExit,
10 generateRandomString, 11 generateRandomString,
11 isTestInstance, 12 isTestInstance,
12 getFormatedObjects 13 getFormatedObjects,
14 transactionRetryer
13} 15}
14 16
15function badRequest (req, res, next) { 17function badRequest (req, res, next) {
@@ -46,6 +48,18 @@ function getFormatedObjects (objects, objectsTotal) {
46 } 48 }
47} 49}
48 50
51function transactionRetryer (func, callback) {
52 retry({
53 times: 5,
54
55 errorFilter: function (err) {
56 const willRetry = (err.name === 'SequelizeDatabaseError')
57 logger.debug('Maybe retrying the transaction function.', { willRetry })
58 return willRetry
59 }
60 }, func, callback)
61}
62
49// --------------------------------------------------------------------------- 63// ---------------------------------------------------------------------------
50 64
51module.exports = utils 65module.exports = utils
diff --git a/server/lib/friends.js b/server/lib/friends.js
index 4afb91b8b..3d3d0fdee 100644
--- a/server/lib/friends.js
+++ b/server/lib/friends.js
@@ -24,16 +24,33 @@ const friends = {
24 sendOwnedVideosToPod 24 sendOwnedVideosToPod
25} 25}
26 26
27function addVideoToFriends (videoData) { 27function addVideoToFriends (videoData, transaction, callback) {
28 createRequest('add', constants.REQUEST_ENDPOINTS.VIDEOS, videoData) 28 const options = {
29 type: 'add',
30 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
31 data: videoData,
32 transaction
33 }
34 createRequest(options, callback)
29} 35}
30 36
31function updateVideoToFriends (videoData) { 37function updateVideoToFriends (videoData, transaction, callback) {
32 createRequest('update', constants.REQUEST_ENDPOINTS.VIDEOS, videoData) 38 const options = {
39 type: 'update',
40 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
41 data: videoData,
42 transaction
43 }
44 createRequest(options, callback)
33} 45}
34 46
35function removeVideoToFriends (videoParams) { 47function removeVideoToFriends (videoParams) {
36 createRequest('remove', constants.REQUEST_ENDPOINTS.VIDEOS, videoParams) 48 const options = {
49 type: 'remove',
50 endpoint: constants.REQUEST_ENDPOINTS.VIDEOS,
51 data: videoParams
52 }
53 createRequest(options)
37} 54}
38 55
39function reportAbuseVideoToFriend (reportData, video) { 56function reportAbuseVideoToFriend (reportData, video) {
@@ -258,25 +275,35 @@ function makeRequestsToWinningPods (cert, podsList, callback) {
258} 275}
259 276
260// Wrapper that populate "toIds" argument with all our friends if it is not specified 277// Wrapper that populate "toIds" argument with all our friends if it is not specified
261function createRequest (type, endpoint, data, toIds) { 278// { type, endpoint, data, toIds, transaction }
262 if (toIds) return _createRequest(type, endpoint, data, toIds) 279function createRequest (options, callback) {
280 if (!callback) callback = function () {}
281 if (options.toIds) return _createRequest(options, callback)
263 282
264 // If the "toIds" pods is not specified, we send the request to all our friends 283 // If the "toIds" pods is not specified, we send the request to all our friends
265 db.Pod.listAllIds(function (err, podIds) { 284 db.Pod.listAllIds(options.transaction, function (err, podIds) {
266 if (err) { 285 if (err) {
267 logger.error('Cannot get pod ids', { error: err }) 286 logger.error('Cannot get pod ids', { error: err })
268 return 287 return
269 } 288 }
270 289
271 return _createRequest(type, endpoint, data, podIds) 290 const newOptions = Object.assign(options, { toIds: podIds })
291 return _createRequest(newOptions, callback)
272 }) 292 })
273} 293}
274 294
275function _createRequest (type, endpoint, data, toIds) { 295// { type, endpoint, data, toIds, transaction }
296function _createRequest (options, callback) {
297 const type = options.type
298 const endpoint = options.endpoint
299 const data = options.data
300 const toIds = options.toIds
301 const transaction = options.transaction
302
276 const pods = [] 303 const pods = []
277 304
278 // If there are no destination pods abort 305 // If there are no destination pods abort
279 if (toIds.length === 0) return 306 if (toIds.length === 0) return callback(null)
280 307
281 toIds.forEach(function (toPod) { 308 toIds.forEach(function (toPod) {
282 pods.push(db.Pod.build({ id: toPod })) 309 pods.push(db.Pod.build({ id: toPod }))
@@ -290,17 +317,14 @@ function _createRequest (type, endpoint, data, toIds) {
290 } 317 }
291 } 318 }
292 319
293 // We run in transaction to keep coherency between Request and RequestToPod tables 320 const dbRequestOptions = {
294 db.sequelize.transaction(function (t) { 321 transaction
295 const dbRequestOptions = { 322 }
296 transaction: t
297 }
298 323
299 return db.Request.create(createQuery, dbRequestOptions).then(function (request) { 324 return db.Request.create(createQuery, dbRequestOptions).asCallback(function (err, request) {
300 return request.setPods(pods, dbRequestOptions) 325 if (err) return callback(err)
301 }) 326
302 }).asCallback(function (err) { 327 return request.setPods(pods, dbRequestOptions).asCallback(callback)
303 if (err) logger.error('Error in createRequest transaction.', { error: err })
304 }) 328 })
305} 329}
306 330
diff --git a/server/models/pod.js b/server/models/pod.js
index 83ecd732e..8e7dd1fd8 100644
--- a/server/models/pod.js
+++ b/server/models/pod.js
@@ -115,11 +115,18 @@ function list (callback) {
115 return this.findAll().asCallback(callback) 115 return this.findAll().asCallback(callback)
116} 116}
117 117
118function listAllIds (callback) { 118function listAllIds (transaction, callback) {
119 if (!callback) {
120 callback = transaction
121 transaction = null
122 }
123
119 const query = { 124 const query = {
120 attributes: [ 'id' ] 125 attributes: [ 'id' ]
121 } 126 }
122 127
128 if (transaction) query.transaction = transaction
129
123 return this.findAll(query).asCallback(function (err, pods) { 130 return this.findAll(query).asCallback(function (err, pods) {
124 if (err) return callback(err) 131 if (err) return callback(err)
125 132
diff --git a/server/models/request.js b/server/models/request.js
index bae227c05..1d6038044 100644
--- a/server/models/request.js
+++ b/server/models/request.js
@@ -291,8 +291,8 @@ function listWithLimitAndRandom (limit, callback) {
291 order: [ 291 order: [
292 [ 'id', 'ASC' ] 292 [ 'id', 'ASC' ]
293 ], 293 ],
294 offset: start, 294 // offset: start,
295 limit: limit, 295 // limit: limit,
296 include: [ this.sequelize.models.Pod ] 296 include: [ this.sequelize.models.Pod ]
297 } 297 }
298 298