このコミットが含まれているのは:
@@ -6,11 +6,12 @@ import type { PostImportOrigin,
|
||||
PostImportSkipReason,
|
||||
StorageErrorHandler } from '@/lib/postImportTypes'
|
||||
|
||||
const SESSION_VERSION = 2
|
||||
const SESSION_VERSION = 3
|
||||
const SESSION_PREFIX = 'post-import-session:'
|
||||
const CURRENT_SESSION_KEY = `${ SESSION_PREFIX }current`
|
||||
const SOURCE_DRAFT_KEY = 'post-import-source-draft'
|
||||
const SESSION_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
||||
const SESSION_ID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
const ATTRIBUTE_KEYS = [
|
||||
'title',
|
||||
'thumbnailBase',
|
||||
@@ -29,7 +30,21 @@ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value != null && !(Array.isArray (value))
|
||||
|
||||
|
||||
const sessionKey = (): string => CURRENT_SESSION_KEY
|
||||
export const validatePostImportSessionId = (value: unknown): string | null => {
|
||||
if (typeof value !== 'string')
|
||||
return null
|
||||
if (value.length > 36 || !(SESSION_ID_PATTERN.test (value)))
|
||||
return null
|
||||
|
||||
return value.toLowerCase ()
|
||||
}
|
||||
|
||||
|
||||
export const generatePostImportSessionId = (): string =>
|
||||
crypto.randomUUID ()
|
||||
|
||||
|
||||
const sessionKey = (sessionId: string): string => `${ SESSION_PREFIX }${ sessionId }`
|
||||
|
||||
|
||||
const readStorage = (
|
||||
@@ -283,9 +298,9 @@ const sanitiseRow = (value: unknown): PostImportRow | null => {
|
||||
}
|
||||
|
||||
|
||||
const isExpiredSession = (savedAt: string): boolean => {
|
||||
const value = Date.parse (savedAt)
|
||||
return Number.isNaN (value) || Date.now () - value > SESSION_MAX_AGE_MS
|
||||
const isExpiredSession = (expiresAt: string): boolean => {
|
||||
const value = Date.parse (expiresAt)
|
||||
return Number.isNaN (value) || value <= Date.now ()
|
||||
}
|
||||
|
||||
|
||||
@@ -303,20 +318,25 @@ export const cleanupExpiredPostImportSessions = (
|
||||
if (key == null || !(key.startsWith (SESSION_PREFIX)))
|
||||
continue
|
||||
|
||||
const raw = sessionStorage.getItem (key)
|
||||
if (raw == null)
|
||||
continue
|
||||
|
||||
if (key !== CURRENT_SESSION_KEY)
|
||||
const sessionId = validatePostImportSessionId (key.slice (SESSION_PREFIX.length))
|
||||
if (sessionId == null)
|
||||
{
|
||||
sessionStorage.removeItem (key)
|
||||
--i
|
||||
continue
|
||||
}
|
||||
|
||||
const raw = sessionStorage.getItem (key)
|
||||
if (raw == null)
|
||||
continue
|
||||
|
||||
try
|
||||
{
|
||||
const value = JSON.parse (raw) as { savedAt?: string }
|
||||
if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt))
|
||||
const value = JSON.parse (raw) as { expiresAt?: string
|
||||
rows?: unknown[] }
|
||||
if (typeof value.expiresAt !== 'string'
|
||||
|| isExpiredSession (value.expiresAt)
|
||||
|| !(Array.isArray (value.rows)))
|
||||
{
|
||||
sessionStorage.removeItem (key)
|
||||
--i
|
||||
@@ -370,38 +390,33 @@ export const clearPostImportSourceDraft = (
|
||||
|
||||
|
||||
export const savePostImportSession = (
|
||||
sessionOrLegacyId: string | Omit<PostImportSession, 'version' | 'savedAt'>,
|
||||
sessionOrOnError?: Omit<PostImportSession, 'version' | 'savedAt'> | StorageErrorHandler,
|
||||
sessionId: string,
|
||||
session: Omit<PostImportSession, 'version' | 'expiresAt'>,
|
||||
onError?: StorageErrorHandler,
|
||||
): boolean => {
|
||||
const session =
|
||||
typeof sessionOrLegacyId === 'string'
|
||||
? sessionOrOnError as Omit<PostImportSession, 'version' | 'savedAt'>
|
||||
: sessionOrLegacyId
|
||||
const errorHandler =
|
||||
typeof sessionOrLegacyId === 'string'
|
||||
? onError
|
||||
: sessionOrOnError as StorageErrorHandler | undefined
|
||||
const validatedId = validatePostImportSessionId (sessionId)
|
||||
if (validatedId == null)
|
||||
return false
|
||||
|
||||
return writeStorage (
|
||||
sessionKey (),
|
||||
sessionKey (validatedId),
|
||||
JSON.stringify ({
|
||||
...session,
|
||||
version: SESSION_VERSION,
|
||||
savedAt: new Date ().toISOString () }),
|
||||
errorHandler)
|
||||
expiresAt: new Date (Date.now () + SESSION_MAX_AGE_MS).toISOString () }),
|
||||
onError)
|
||||
}
|
||||
|
||||
|
||||
export const loadPostImportSession = (
|
||||
legacyIdOrOnError?: string | StorageErrorHandler,
|
||||
sessionId: string,
|
||||
onError?: StorageErrorHandler,
|
||||
): PostImportSession | null => {
|
||||
const errorHandler =
|
||||
typeof legacyIdOrOnError === 'string'
|
||||
? onError
|
||||
: legacyIdOrOnError
|
||||
const raw = readStorage (sessionKey (), errorHandler)
|
||||
const validatedId = validatePostImportSessionId (sessionId)
|
||||
if (validatedId == null)
|
||||
return null
|
||||
|
||||
const raw = readStorage (sessionKey (validatedId), onError)
|
||||
if (raw == null)
|
||||
return null
|
||||
|
||||
@@ -410,9 +425,9 @@ export const loadPostImportSession = (
|
||||
const value = JSON.parse (raw) as Partial<PostImportSession>
|
||||
if (value.version !== SESSION_VERSION || !(Array.isArray (value.rows)))
|
||||
return null
|
||||
if (typeof value.savedAt !== 'string' || isExpiredSession (value.savedAt))
|
||||
if (typeof value.expiresAt !== 'string' || isExpiredSession (value.expiresAt))
|
||||
{
|
||||
removeStorage (sessionKey (), errorHandler)
|
||||
removeStorage (sessionKey (validatedId), onError)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -422,7 +437,7 @@ export const loadPostImportSession = (
|
||||
|
||||
return {
|
||||
version: SESSION_VERSION,
|
||||
savedAt: value.savedAt,
|
||||
expiresAt: value.expiresAt,
|
||||
source: typeof value.source === 'string' ? value.source : '',
|
||||
rows: rows as PostImportRow[],
|
||||
repairMode: value.repairMode === 'failed' ? 'failed' : 'all' }
|
||||
@@ -435,7 +450,12 @@ export const loadPostImportSession = (
|
||||
|
||||
|
||||
export const clearPostImportSession = (
|
||||
sessionId: string,
|
||||
onError?: StorageErrorHandler,
|
||||
) => {
|
||||
removeStorage (sessionKey (), onError)
|
||||
const validatedId = validatePostImportSessionId (sessionId)
|
||||
if (validatedId == null)
|
||||
return
|
||||
|
||||
removeStorage (sessionKey (validatedId), onError)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ export type PostImportRow = {
|
||||
resetSnapshot: PostImportResetSnapshot
|
||||
createdPostId?: number
|
||||
importStatus?: PostImportStatus
|
||||
recoverable?: boolean }
|
||||
recoverable?: boolean
|
||||
thumbnailFile?: File
|
||||
thumbnailObjectUrl?: string }
|
||||
|
||||
export type PostImportResultRow =
|
||||
| {
|
||||
@@ -61,11 +63,11 @@ export type PostImportResultRow =
|
||||
recoverable?: boolean }
|
||||
|
||||
export type PostImportSession = {
|
||||
version: number
|
||||
savedAt: string
|
||||
source: string
|
||||
rows: PostImportRow[]
|
||||
repairMode: PostImportRepairMode }
|
||||
version: number
|
||||
expiresAt: string
|
||||
source: string
|
||||
rows: PostImportRow[]
|
||||
repairMode: PostImportRepairMode }
|
||||
|
||||
export type PostImportSourceIssue = {
|
||||
sourceRow: number
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { resultRepairMode } from '@/lib/postImportRows'
|
||||
import { validatePostImportSessionId } from '@/lib/postImportStorage'
|
||||
|
||||
import type {
|
||||
PostImportOrigin,
|
||||
@@ -12,6 +13,7 @@ type QueryRowState = {
|
||||
thumbnail_base: string
|
||||
original_created_from: string
|
||||
original_created_before: string
|
||||
video_ms: string
|
||||
duration: string
|
||||
tags: string
|
||||
parent_post_ids: string
|
||||
@@ -37,6 +39,7 @@ type EncodedRowState = {
|
||||
h?: string
|
||||
f?: string
|
||||
b?: string
|
||||
x?: string
|
||||
d?: string
|
||||
g?: string
|
||||
p?: string
|
||||
@@ -58,8 +61,8 @@ export type ParsedPostNewState = {
|
||||
shortcut: boolean }
|
||||
|
||||
export type SerialisedPostNewState = {
|
||||
path: string
|
||||
rows: PostImportRow[] }
|
||||
path: string
|
||||
complete: boolean }
|
||||
|
||||
const BASIC_KEYS = [
|
||||
'url',
|
||||
@@ -67,6 +70,7 @@ const BASIC_KEYS = [
|
||||
'thumbnail_base',
|
||||
'original_created_from',
|
||||
'original_created_before',
|
||||
'video_ms',
|
||||
'duration',
|
||||
'tags',
|
||||
'parent_post_ids'] as const
|
||||
@@ -77,7 +81,7 @@ const STATE_KEYS = [
|
||||
'import_status',
|
||||
'created_post_id',
|
||||
'recoverable'] as const
|
||||
const SINGLE_ROW_KEYS = [...BASIC_KEYS, ...STATE_KEYS] as const
|
||||
const SINGLE_ROW_KEYS = [...BASIC_KEYS, ...STATE_KEYS, 'session_id'] as const
|
||||
const MAX_REGULAR_URL_LENGTH = 768
|
||||
const MAX_COMPRESSED_STATE_LENGTH = 767
|
||||
const COMPRESSED_STATE_PREFIX = 'z1.'
|
||||
@@ -241,6 +245,7 @@ const baseRowState = (row: PostImportRow): QueryRowState => ({
|
||||
thumbnail_base: String (row.attributes.thumbnailBase ?? ''),
|
||||
original_created_from: String (row.attributes.originalCreatedFrom ?? ''),
|
||||
original_created_before: String (row.attributes.originalCreatedBefore ?? ''),
|
||||
video_ms: String (row.attributes.videoMs ?? ''),
|
||||
duration: String (row.attributes.duration ?? ''),
|
||||
tags: String (row.attributes.tags ?? ''),
|
||||
parent_post_ids: String (row.attributes.parentPostIds ?? ''),
|
||||
@@ -263,6 +268,7 @@ const buildRow = (
|
||||
thumbnailBase: manual.has ('thumbnailBase') ? 'manual' : 'automatic',
|
||||
originalCreatedFrom: manual.has ('originalCreatedFrom') ? 'manual' : 'automatic',
|
||||
originalCreatedBefore: manual.has ('originalCreatedBefore') ? 'manual' : 'automatic',
|
||||
videoMs: 'automatic',
|
||||
duration: 'automatic',
|
||||
tags: manual.has ('tags') ? 'manual' : 'automatic',
|
||||
parentPostIds: manual.has ('parentPostIds') ? 'manual' : 'automatic' }
|
||||
@@ -274,6 +280,7 @@ const buildRow = (
|
||||
thumbnailBase: state.thumbnail_base,
|
||||
originalCreatedFrom: state.original_created_from,
|
||||
originalCreatedBefore: state.original_created_before,
|
||||
videoMs: state.video_ms,
|
||||
duration: state.duration,
|
||||
tags: state.tags,
|
||||
parentPostIds: state.parent_post_ids }
|
||||
@@ -356,6 +363,7 @@ const parseEncodedRow = (
|
||||
['h', 'thumbnail_base'],
|
||||
['f', 'original_created_from'],
|
||||
['b', 'original_created_before'],
|
||||
['x', 'video_ms'],
|
||||
['d', 'duration'],
|
||||
['g', 'tags'],
|
||||
['p', 'parent_post_ids']] as const
|
||||
@@ -382,6 +390,7 @@ const parseEncodedRow = (
|
||||
thumbnail_base: parsedStrings.thumbnail_base,
|
||||
original_created_from: parsedStrings.original_created_from,
|
||||
original_created_before: parsedStrings.original_created_before,
|
||||
video_ms: parsedStrings.video_ms,
|
||||
duration: parsedStrings.duration,
|
||||
tags: parsedStrings.tags,
|
||||
parent_post_ids: parsedStrings.parent_post_ids,
|
||||
@@ -399,7 +408,8 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
if (url == null || url === '')
|
||||
return null
|
||||
|
||||
const hasOnlyUrl = Array.from (params.keys ()).every (key => key === 'url')
|
||||
const hasOnlyUrl = Array.from (params.keys ()).every (
|
||||
key => key === 'url' || key === 'session_id')
|
||||
if (hasOnlyUrl)
|
||||
{
|
||||
const row = buildRow (1, {
|
||||
@@ -408,6 +418,7 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
thumbnail_base: '',
|
||||
original_created_from: '',
|
||||
original_created_before: '',
|
||||
video_ms: '',
|
||||
duration: '',
|
||||
tags: '',
|
||||
parent_post_ids: '',
|
||||
@@ -444,6 +455,7 @@ const parseSingleRow = (params: URLSearchParams): ParsedPostNewState | null => {
|
||||
thumbnail_base: params.get ('thumbnail_base') ?? '',
|
||||
original_created_from: params.get ('original_created_from') ?? '',
|
||||
original_created_before: params.get ('original_created_before') ?? '',
|
||||
video_ms: params.get ('video_ms') ?? '',
|
||||
duration: params.get ('duration') ?? '',
|
||||
tags: params.get ('tags') ?? '',
|
||||
parent_post_ids: params.get ('parent_post_ids') ?? '',
|
||||
@@ -546,6 +558,7 @@ const regularQuery = (rows: PostImportRow[]): URLSearchParams => {
|
||||
params.set ('thumbnail_base', String (row?.attributes.thumbnailBase ?? ''))
|
||||
params.set ('original_created_from', String (row?.attributes.originalCreatedFrom ?? ''))
|
||||
params.set ('original_created_before', String (row?.attributes.originalCreatedBefore ?? ''))
|
||||
params.set ('video_ms', String (row?.attributes.videoMs ?? ''))
|
||||
params.set ('duration', String (row?.attributes.duration ?? ''))
|
||||
params.set ('tags', String (row?.attributes.tags ?? ''))
|
||||
params.set ('parent_post_ids', String (row?.attributes.parentPostIds ?? ''))
|
||||
@@ -589,6 +602,8 @@ const encodeRowState = (
|
||||
encoded.f = base.original_created_from
|
||||
if (base.original_created_before !== '')
|
||||
encoded.b = base.original_created_before
|
||||
if (base.video_ms !== '')
|
||||
encoded.x = base.video_ms
|
||||
if (base.duration !== '')
|
||||
encoded.d = base.duration
|
||||
if (base.tags !== '')
|
||||
@@ -626,6 +641,24 @@ export const hasPostNewReviewState = (search: string): boolean => {
|
||||
}
|
||||
|
||||
|
||||
export const parsePostNewSessionId = (search: string): string | null =>
|
||||
validatePostImportSessionId (
|
||||
new URLSearchParams (search).get ('session_id'))
|
||||
|
||||
|
||||
export const appendPostNewSessionId = (
|
||||
path: string,
|
||||
sessionId: string,
|
||||
): string => {
|
||||
const validatedId = validatePostImportSessionId (sessionId)
|
||||
if (validatedId == null)
|
||||
return path
|
||||
|
||||
const separator = path.includes ('?') ? '&' : '?'
|
||||
return `${ path }${ separator }session_id=${ validatedId }`
|
||||
}
|
||||
|
||||
|
||||
export const parsePostNewState = async (
|
||||
search: string,
|
||||
): Promise<ParsedPostNewState | null> => {
|
||||
@@ -661,7 +694,7 @@ const serialiseCompressedRows = async (
|
||||
if (payload.length <= MAX_COMPRESSED_STATE_LENGTH)
|
||||
return {
|
||||
path: `/posts/new?state=${ state }`,
|
||||
rows: nextRows }
|
||||
complete: count === rows.length }
|
||||
}
|
||||
}
|
||||
catch
|
||||
@@ -680,7 +713,7 @@ export const serialisePostNewState = async (
|
||||
if (regularPath.length < MAX_REGULAR_URL_LENGTH)
|
||||
return {
|
||||
path: regularPath,
|
||||
rows }
|
||||
complete: true }
|
||||
|
||||
return await serialiseCompressedRows (rows)
|
||||
}
|
||||
|
||||
新しいイシューから参照
ユーザーをブロックする