1package usecase
2
3import (
4 "context"
5 "fmt"
6
7 "github.com/bnema/dumber/internal/application/port"
8 "github.com/bnema/dumber/internal/domain/entity"
9 "github.com/bnema/dumber/internal/logging"
10)
11
12// PurgeDataUseCase handles discovering and purging application data.
13type PurgeDataUseCase struct {
14 fs port.FileSystem
15 xdg port.XDGPaths
16 desktop port.DesktopIntegration
17}
18
19// NewPurgeDataUseCase creates a new PurgeDataUseCase.
20func NewPurgeDataUseCase(fs port.FileSystem, xdg port.XDGPaths, desktop port.DesktopIntegration) *PurgeDataUseCase {
21 return &PurgeDataUseCase{fs: fs, xdg: xdg, desktop: desktop}
22}
23
24// GetPurgeTargets returns all available purge targets with their current state.
25func (uc *PurgeDataUseCase) GetPurgeTargets(ctx context.Context) ([]entity.PurgeTarget, error) {
26 configDir, err := uc.xdg.ConfigDir()
27 if err != nil {
28 return nil, err
29 }
30 dataDir, err := uc.xdg.DataDir()
31 if err != nil {
32 return nil, err
33 }
34 stateDir, err := uc.xdg.StateDir()
35 if err != nil {
36 return nil, err
37 }
38 cacheDir, err := uc.xdg.CacheDir()
39 if err != nil {
40 return nil, err
41 }
42 filterJSONDir, err := uc.xdg.FilterJSONDir()
43 if err != nil {
44 return nil, err
45 }
46 filterStoreDir, err := uc.xdg.FilterStoreDir()
47 if err != nil {
48 return nil, err
49 }
50 filterCacheDir, err := uc.xdg.FilterCacheDir()
51 if err != nil {
52 return nil, err
53 }
54
55 baseTargets := []entity.PurgeTarget{
56 {Type: entity.PurgeTargetConfig, Path: configDir, Description: "config"},
57 {Type: entity.PurgeTargetData, Path: dataDir, Description: "data"},
58 {Type: entity.PurgeTargetState, Path: stateDir, Description: "state"},
59 {Type: entity.PurgeTargetCache, Path: cacheDir, Description: "cache"},
60 {Type: entity.PurgeTargetFilterJSON, Path: filterJSONDir, Description: "filter JSON cache"},
61 {Type: entity.PurgeTargetFilterStore, Path: filterStoreDir, Description: "compiled filters"},
62 {Type: entity.PurgeTargetFilterCache, Path: filterCacheDir, Description: "filter cache"},
63 }
64
65 status, statusErr := uc.desktop.GetStatus(ctx)
66 if statusErr != nil {
67 // Desktop status should not block purge targets discovery.
68 logging.FromContext(ctx).Warn().Err(statusErr).Msg("failed to get desktop integration status")
69 }
70
71 if status != nil {
72 baseTargets = append(baseTargets,
73 entity.PurgeTarget{Type: entity.PurgeTargetDesktopFile, Path: status.DesktopFilePath, Description: "desktop file"},
74 entity.PurgeTarget{Type: entity.PurgeTargetIcon, Path: status.IconFilePath, Description: "icon"},
75 )
76 }
77
78 var targets []entity.PurgeTarget
79 for _, t := range baseTargets {
80 if t.Path == "" {
81 t.Exists = false
82 t.Size = 0
83 targets = append(targets, t)
84 continue
85 }
86 exists, err := uc.fs.Exists(ctx, t.Path)
87 if err != nil {
88 return nil, err
89 }
90 t.Exists = exists
91 if !exists {
92 t.Size = 0
93 targets = append(targets, t)
94 continue
95 }
96 size, err := uc.fs.GetSize(ctx, t.Path)
97 if err != nil {
98 return nil, err
99 }
100 t.Size = size
101 targets = append(targets, t)
102 }
103
104 return targets, nil
105}
106
107// PurgeInput specifies which target types to purge.
108type PurgeInput struct {
109 TargetTypes []entity.PurgeTargetType
110}
111
112// PurgeOutput contains the results of the purge operation.
113type PurgeOutput struct {
114 Results []entity.PurgeResult
115 TotalSize int64
116 SuccessCount int
117 FailureCount int
118}
119
120// Execute purges the selected target types.
121// Continues on errors, collecting all results.
122func (uc *PurgeDataUseCase) Execute(ctx context.Context, input PurgeInput) (*PurgeOutput, error) {
123 log := logging.FromContext(ctx)
124
125 targets, err := uc.GetPurgeTargets(ctx)
126 if err != nil {
127 return nil, err
128 }
129
130 selected := make(map[entity.PurgeTargetType]struct{}, len(input.TargetTypes))
131 for _, tt := range input.TargetTypes {
132 selected[tt] = struct{}{}
133 }
134
135 out := &PurgeOutput{}
136 for _, t := range targets {
137 if _, ok := selected[t.Type]; !ok {
138 continue
139 }
140 if !t.Exists {
141 continue
142 }
143
144 res := entity.PurgeResult{Target: t}
145 out.TotalSize += t.Size
146
147 switch t.Type {
148 case entity.PurgeTargetDesktopFile:
149 err = uc.desktop.RemoveDesktopFile(ctx)
150 case entity.PurgeTargetIcon:
151 err = uc.desktop.RemoveIcon(ctx)
152 default:
153 err = uc.fs.RemoveAll(ctx, t.Path)
154 }
155
156 if err != nil {
157 res.Success = false
158 res.Error = err
159 out.FailureCount++
160 log.Warn().Err(err).Str("path", t.Path).Int("type", int(t.Type)).Msg("purge target failed")
161 } else {
162 res.Success = true
163 out.SuccessCount++
164 log.Info().Str("path", t.Path).Int("type", int(t.Type)).Msg("purge target removed")
165 }
166
167 out.Results = append(out.Results, res)
168 }
169
170 if out.FailureCount > 0 {
171 return out, fmt.Errorf("failed to remove %d items", out.FailureCount)
172 }
173 return out, nil
174}
175
176// PurgeAll purges all existing targets (for --force mode).
177func (uc *PurgeDataUseCase) PurgeAll(ctx context.Context) (*PurgeOutput, error) {
178 targets, err := uc.GetPurgeTargets(ctx)
179 if err != nil {
180 return nil, err
181 }
182
183 var types []entity.PurgeTargetType
184 for _, t := range targets {
185 types = append(types, t.Type)
186 }
187
188 return uc.Execute(ctx, PurgeInput{TargetTypes: types})
189}